forked from cerc-io/ipld-eth-server
Add FlipKick log events transformer
This commit is contained in:
+9
@@ -0,0 +1,9 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.4.3
|
||||
- 1.5.3
|
||||
- tip
|
||||
|
||||
script:
|
||||
- go test -v ./...
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
# How to contribute
|
||||
|
||||
We definitely welcome patches and contribution to this project!
|
||||
|
||||
### Legal requirements
|
||||
|
||||
In order to protect both you and ourselves, you will need to sign the
|
||||
[Contributor License Agreement](https://cla.developers.google.com/clas).
|
||||
|
||||
You may have already signed it for other Google projects.
|
||||
+1
@@ -0,0 +1 @@
|
||||
Paul Borman <borman@google.com>
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
Copyright (c) 2009,2014 Google Inc. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
This project was automatically exported from code.google.com/p/go-uuid
|
||||
|
||||
# uuid 
|
||||
The uuid package generates and inspects UUIDs based on [RFC 4122](http://tools.ietf.org/html/rfc4122) and DCE 1.1: Authentication and Security Services.
|
||||
|
||||
###### Install
|
||||
`go get github.com/pborman/uuid`
|
||||
|
||||
###### Documentation
|
||||
[](http://godoc.org/github.com/pborman/uuid)
|
||||
|
||||
Full `go doc` style documentation for the package can be viewed online without installing this package by using the GoDoc site here:
|
||||
http://godoc.org/github.com/pborman/uuid
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
// Copyright 2011 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// A Domain represents a Version 2 domain
|
||||
type Domain byte
|
||||
|
||||
// Domain constants for DCE Security (Version 2) UUIDs.
|
||||
const (
|
||||
Person = Domain(0)
|
||||
Group = Domain(1)
|
||||
Org = Domain(2)
|
||||
)
|
||||
|
||||
// NewDCESecurity returns a DCE Security (Version 2) UUID.
|
||||
//
|
||||
// The domain should be one of Person, Group or Org.
|
||||
// On a POSIX system the id should be the users UID for the Person
|
||||
// domain and the users GID for the Group. The meaning of id for
|
||||
// the domain Org or on non-POSIX systems is site defined.
|
||||
//
|
||||
// For a given domain/id pair the same token may be returned for up to
|
||||
// 7 minutes and 10 seconds.
|
||||
func NewDCESecurity(domain Domain, id uint32) UUID {
|
||||
uuid := NewUUID()
|
||||
if uuid != nil {
|
||||
uuid[6] = (uuid[6] & 0x0f) | 0x20 // Version 2
|
||||
uuid[9] = byte(domain)
|
||||
binary.BigEndian.PutUint32(uuid[0:], id)
|
||||
}
|
||||
return uuid
|
||||
}
|
||||
|
||||
// NewDCEPerson returns a DCE Security (Version 2) UUID in the person
|
||||
// domain with the id returned by os.Getuid.
|
||||
//
|
||||
// NewDCEPerson(Person, uint32(os.Getuid()))
|
||||
func NewDCEPerson() UUID {
|
||||
return NewDCESecurity(Person, uint32(os.Getuid()))
|
||||
}
|
||||
|
||||
// NewDCEGroup returns a DCE Security (Version 2) UUID in the group
|
||||
// domain with the id returned by os.Getgid.
|
||||
//
|
||||
// NewDCEGroup(Group, uint32(os.Getgid()))
|
||||
func NewDCEGroup() UUID {
|
||||
return NewDCESecurity(Group, uint32(os.Getgid()))
|
||||
}
|
||||
|
||||
// Domain returns the domain for a Version 2 UUID or false.
|
||||
func (uuid UUID) Domain() (Domain, bool) {
|
||||
if v, _ := uuid.Version(); v != 2 {
|
||||
return 0, false
|
||||
}
|
||||
return Domain(uuid[9]), true
|
||||
}
|
||||
|
||||
// Id returns the id for a Version 2 UUID or false.
|
||||
func (uuid UUID) Id() (uint32, bool) {
|
||||
if v, _ := uuid.Version(); v != 2 {
|
||||
return 0, false
|
||||
}
|
||||
return binary.BigEndian.Uint32(uuid[0:4]), true
|
||||
}
|
||||
|
||||
func (d Domain) String() string {
|
||||
switch d {
|
||||
case Person:
|
||||
return "Person"
|
||||
case Group:
|
||||
return "Group"
|
||||
case Org:
|
||||
return "Org"
|
||||
}
|
||||
return fmt.Sprintf("Domain%d", int(d))
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// Copyright 2011 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// The uuid package generates and inspects UUIDs.
|
||||
//
|
||||
// UUIDs are based on RFC 4122 and DCE 1.1: Authentication and Security Services.
|
||||
package uuid
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
// Copyright 2011 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"hash"
|
||||
)
|
||||
|
||||
// Well known Name Space IDs and UUIDs
|
||||
var (
|
||||
NameSpace_DNS = Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8")
|
||||
NameSpace_URL = Parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8")
|
||||
NameSpace_OID = Parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8")
|
||||
NameSpace_X500 = Parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8")
|
||||
NIL = Parse("00000000-0000-0000-0000-000000000000")
|
||||
)
|
||||
|
||||
// NewHash returns a new UUID derived from the hash of space concatenated with
|
||||
// data generated by h. The hash should be at least 16 byte in length. The
|
||||
// first 16 bytes of the hash are used to form the UUID. The version of the
|
||||
// UUID will be the lower 4 bits of version. NewHash is used to implement
|
||||
// NewMD5 and NewSHA1.
|
||||
func NewHash(h hash.Hash, space UUID, data []byte, version int) UUID {
|
||||
h.Reset()
|
||||
h.Write(space)
|
||||
h.Write([]byte(data))
|
||||
s := h.Sum(nil)
|
||||
uuid := make([]byte, 16)
|
||||
copy(uuid, s)
|
||||
uuid[6] = (uuid[6] & 0x0f) | uint8((version&0xf)<<4)
|
||||
uuid[8] = (uuid[8] & 0x3f) | 0x80 // RFC 4122 variant
|
||||
return uuid
|
||||
}
|
||||
|
||||
// NewMD5 returns a new MD5 (Version 3) UUID based on the
|
||||
// supplied name space and data.
|
||||
//
|
||||
// NewHash(md5.New(), space, data, 3)
|
||||
func NewMD5(space UUID, data []byte) UUID {
|
||||
return NewHash(md5.New(), space, data, 3)
|
||||
}
|
||||
|
||||
// NewSHA1 returns a new SHA1 (Version 5) UUID based on the
|
||||
// supplied name space and data.
|
||||
//
|
||||
// NewHash(sha1.New(), space, data, 5)
|
||||
func NewSHA1(space UUID, data []byte) UUID {
|
||||
return NewHash(sha1.New(), space, data, 5)
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
// Copyright 2016 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// MarshalText implements encoding.TextMarshaler.
|
||||
func (u UUID) MarshalText() ([]byte, error) {
|
||||
if len(u) != 16 {
|
||||
return nil, nil
|
||||
}
|
||||
var js [36]byte
|
||||
encodeHex(js[:], u)
|
||||
return js[:], nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements encoding.TextUnmarshaler.
|
||||
func (u *UUID) UnmarshalText(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
id := Parse(string(data))
|
||||
if id == nil {
|
||||
return errors.New("invalid UUID")
|
||||
}
|
||||
*u = id
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalBinary implements encoding.BinaryMarshaler.
|
||||
func (u UUID) MarshalBinary() ([]byte, error) {
|
||||
return u[:], nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
|
||||
func (u *UUID) UnmarshalBinary(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(data) != 16 {
|
||||
return fmt.Errorf("invalid UUID (got %d bytes)", len(data))
|
||||
}
|
||||
var id [16]byte
|
||||
copy(id[:], data)
|
||||
*u = id[:]
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalText implements encoding.TextMarshaler.
|
||||
func (u Array) MarshalText() ([]byte, error) {
|
||||
var js [36]byte
|
||||
encodeHex(js[:], u[:])
|
||||
return js[:], nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements encoding.TextUnmarshaler.
|
||||
func (u *Array) UnmarshalText(data []byte) error {
|
||||
id := Parse(string(data))
|
||||
if id == nil {
|
||||
return errors.New("invalid UUID")
|
||||
}
|
||||
*u = id.Array()
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalBinary implements encoding.BinaryMarshaler.
|
||||
func (u Array) MarshalBinary() ([]byte, error) {
|
||||
return u[:], nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary implements encoding.BinaryUnmarshaler.
|
||||
func (u *Array) UnmarshalBinary(data []byte) error {
|
||||
if len(data) != 16 {
|
||||
return fmt.Errorf("invalid UUID (got %d bytes)", len(data))
|
||||
}
|
||||
copy(u[:], data)
|
||||
return nil
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
// Copyright 2014 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var testUUID = Parse("f47ac10b-58cc-0372-8567-0e02b2c3d479")
|
||||
var testArray = testUUID.Array()
|
||||
|
||||
func TestJSON(t *testing.T) {
|
||||
type S struct {
|
||||
ID1 UUID
|
||||
ID2 UUID
|
||||
}
|
||||
s1 := S{ID1: testUUID}
|
||||
data, err := json.Marshal(&s1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var s2 S
|
||||
if err := json.Unmarshal(data, &s2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(&s1, &s2) {
|
||||
t.Errorf("got %#v, want %#v", s2, s1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONArray(t *testing.T) {
|
||||
type S struct {
|
||||
ID1 Array
|
||||
ID2 Array
|
||||
}
|
||||
s1 := S{ID1: testArray}
|
||||
data, err := json.Marshal(&s1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var s2 S
|
||||
if err := json.Unmarshal(data, &s2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(&s1, &s2) {
|
||||
t.Errorf("got %#v, want %#v", s2, s1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshal(t *testing.T) {
|
||||
data, err := testUUID.MarshalBinary()
|
||||
if err != nil {
|
||||
t.Fatalf("MarhsalBinary returned unexpected error %v", err)
|
||||
}
|
||||
if !bytes.Equal(data, testUUID) {
|
||||
t.Fatalf("MarhsalBinary returns %x, want %x", data, testUUID)
|
||||
}
|
||||
var u UUID
|
||||
u.UnmarshalBinary(data)
|
||||
if !Equal(data, u) {
|
||||
t.Fatalf("UnmarhsalBinary returns %v, want %v", u, testUUID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalArray(t *testing.T) {
|
||||
data, err := testArray.MarshalBinary()
|
||||
if err != nil {
|
||||
t.Fatalf("MarhsalBinary returned unexpected error %v", err)
|
||||
}
|
||||
if !bytes.Equal(data, testUUID) {
|
||||
t.Fatalf("MarhsalBinary returns %x, want %x", data, testUUID)
|
||||
}
|
||||
var a Array
|
||||
a.UnmarshalBinary(data)
|
||||
if a != testArray {
|
||||
t.Fatalf("UnmarhsalBinary returns %v, want %v", a, testArray)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalTextArray(t *testing.T) {
|
||||
data, err := testArray.MarshalText()
|
||||
if err != nil {
|
||||
t.Fatalf("MarhsalText returned unexpected error %v", err)
|
||||
}
|
||||
var a Array
|
||||
a.UnmarshalText(data)
|
||||
if a != testArray {
|
||||
t.Fatalf("UnmarhsalText returns %v, want %v", a, testArray)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUUID_MarshalJSON(b *testing.B) {
|
||||
x := &struct {
|
||||
UUID UUID `json:"uuid"`
|
||||
}{}
|
||||
x.UUID = Parse("f47ac10b-58cc-0372-8567-0e02b2c3d479")
|
||||
if x.UUID == nil {
|
||||
b.Fatal("invalid uuid")
|
||||
}
|
||||
for i := 0; i < b.N; i++ {
|
||||
js, err := json.Marshal(x)
|
||||
if err != nil {
|
||||
b.Fatalf("marshal json: %#v (%v)", js, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUUID_UnmarshalJSON(b *testing.B) {
|
||||
js := []byte(`{"uuid":"f47ac10b-58cc-0372-8567-0e02b2c3d479"}`)
|
||||
var x *struct {
|
||||
UUID UUID `json:"uuid"`
|
||||
}
|
||||
for i := 0; i < b.N; i++ {
|
||||
err := json.Unmarshal(js, &x)
|
||||
if err != nil {
|
||||
b.Fatalf("marshal json: %#v (%v)", js, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
// Copyright 2011 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
nodeMu sync.Mutex
|
||||
interfaces []net.Interface // cached list of interfaces
|
||||
ifname string // name of interface being used
|
||||
nodeID []byte // hardware for version 1 UUIDs
|
||||
)
|
||||
|
||||
// NodeInterface returns the name of the interface from which the NodeID was
|
||||
// derived. The interface "user" is returned if the NodeID was set by
|
||||
// SetNodeID.
|
||||
func NodeInterface() string {
|
||||
defer nodeMu.Unlock()
|
||||
nodeMu.Lock()
|
||||
return ifname
|
||||
}
|
||||
|
||||
// SetNodeInterface selects the hardware address to be used for Version 1 UUIDs.
|
||||
// If name is "" then the first usable interface found will be used or a random
|
||||
// Node ID will be generated. If a named interface cannot be found then false
|
||||
// is returned.
|
||||
//
|
||||
// SetNodeInterface never fails when name is "".
|
||||
func SetNodeInterface(name string) bool {
|
||||
defer nodeMu.Unlock()
|
||||
nodeMu.Lock()
|
||||
return setNodeInterface(name)
|
||||
}
|
||||
|
||||
func setNodeInterface(name string) bool {
|
||||
if interfaces == nil {
|
||||
var err error
|
||||
interfaces, err = net.Interfaces()
|
||||
if err != nil && name != "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
for _, ifs := range interfaces {
|
||||
if len(ifs.HardwareAddr) >= 6 && (name == "" || name == ifs.Name) {
|
||||
if setNodeID(ifs.HardwareAddr) {
|
||||
ifname = ifs.Name
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We found no interfaces with a valid hardware address. If name
|
||||
// does not specify a specific interface generate a random Node ID
|
||||
// (section 4.1.6)
|
||||
if name == "" {
|
||||
if nodeID == nil {
|
||||
nodeID = make([]byte, 6)
|
||||
}
|
||||
randomBits(nodeID)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NodeID returns a slice of a copy of the current Node ID, setting the Node ID
|
||||
// if not already set.
|
||||
func NodeID() []byte {
|
||||
defer nodeMu.Unlock()
|
||||
nodeMu.Lock()
|
||||
if nodeID == nil {
|
||||
setNodeInterface("")
|
||||
}
|
||||
nid := make([]byte, 6)
|
||||
copy(nid, nodeID)
|
||||
return nid
|
||||
}
|
||||
|
||||
// SetNodeID sets the Node ID to be used for Version 1 UUIDs. The first 6 bytes
|
||||
// of id are used. If id is less than 6 bytes then false is returned and the
|
||||
// Node ID is not set.
|
||||
func SetNodeID(id []byte) bool {
|
||||
defer nodeMu.Unlock()
|
||||
nodeMu.Lock()
|
||||
if setNodeID(id) {
|
||||
ifname = "user"
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func setNodeID(id []byte) bool {
|
||||
if len(id) < 6 {
|
||||
return false
|
||||
}
|
||||
if nodeID == nil {
|
||||
nodeID = make([]byte, 6)
|
||||
}
|
||||
copy(nodeID, id)
|
||||
return true
|
||||
}
|
||||
|
||||
// NodeID returns the 6 byte node id encoded in uuid. It returns nil if uuid is
|
||||
// not valid. The NodeID is only well defined for version 1 and 2 UUIDs.
|
||||
func (uuid UUID) NodeID() []byte {
|
||||
if len(uuid) != 16 {
|
||||
return nil
|
||||
}
|
||||
node := make([]byte, 6)
|
||||
copy(node, uuid[10:])
|
||||
return node
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
// Copyright 2014 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// This test is only run when --regressions is passed on the go test line.
|
||||
var regressions = flag.Bool("regressions", false, "run uuid regression tests")
|
||||
|
||||
// TestClockSeqRace tests for a particular race condition of returning two
|
||||
// identical Version1 UUIDs. The duration of 1 minute was chosen as the race
|
||||
// condition, before being fixed, nearly always occured in under 30 seconds.
|
||||
func TestClockSeqRace(t *testing.T) {
|
||||
if !*regressions {
|
||||
t.Skip("skipping regression tests")
|
||||
}
|
||||
duration := time.Minute
|
||||
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
|
||||
ch := make(chan UUID, 10000)
|
||||
ncpu := runtime.NumCPU()
|
||||
switch ncpu {
|
||||
case 0, 1:
|
||||
// We can't run the test effectively.
|
||||
t.Skip("skipping race test, only one CPU detected")
|
||||
return
|
||||
default:
|
||||
runtime.GOMAXPROCS(ncpu)
|
||||
}
|
||||
for i := 0; i < ncpu; i++ {
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case ch <- NewUUID():
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
uuids := make(map[string]bool)
|
||||
cnt := 0
|
||||
start := time.Now()
|
||||
for u := range ch {
|
||||
s := u.String()
|
||||
if uuids[s] {
|
||||
t.Errorf("duplicate uuid after %d in %v: %s", cnt, time.Since(start), s)
|
||||
return
|
||||
}
|
||||
uuids[s] = true
|
||||
if time.Since(start) > duration {
|
||||
return
|
||||
}
|
||||
cnt++
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
// Copyright 2015 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Scan implements sql.Scanner so UUIDs can be read from databases transparently
|
||||
// Currently, database types that map to string and []byte are supported. Please
|
||||
// consult database-specific driver documentation for matching types.
|
||||
func (uuid *UUID) Scan(src interface{}) error {
|
||||
switch src.(type) {
|
||||
case string:
|
||||
// if an empty UUID comes from a table, we return a null UUID
|
||||
if src.(string) == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// see uuid.Parse for required string format
|
||||
parsed := Parse(src.(string))
|
||||
|
||||
if parsed == nil {
|
||||
return errors.New("Scan: invalid UUID format")
|
||||
}
|
||||
|
||||
*uuid = parsed
|
||||
case []byte:
|
||||
b := src.([]byte)
|
||||
|
||||
// if an empty UUID comes from a table, we return a null UUID
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// assumes a simple slice of bytes if 16 bytes
|
||||
// otherwise attempts to parse
|
||||
if len(b) == 16 {
|
||||
*uuid = UUID(b)
|
||||
} else {
|
||||
u := Parse(string(b))
|
||||
|
||||
if u == nil {
|
||||
return errors.New("Scan: invalid UUID format")
|
||||
}
|
||||
|
||||
*uuid = u
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("Scan: unable to scan type %T into UUID", src)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements sql.Valuer so that UUIDs can be written to databases
|
||||
// transparently. Currently, UUIDs map to strings. Please consult
|
||||
// database-specific driver documentation for matching types.
|
||||
func (uuid UUID) Value() (driver.Value, error) {
|
||||
return uuid.String(), nil
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
// Copyright 2015 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestScan(t *testing.T) {
|
||||
var stringTest string = "f47ac10b-58cc-0372-8567-0e02b2c3d479"
|
||||
var byteTest []byte = Parse(stringTest)
|
||||
var badTypeTest int = 6
|
||||
var invalidTest string = "f47ac10b-58cc-0372-8567-0e02b2c3d4"
|
||||
|
||||
// sunny day tests
|
||||
|
||||
var uuid UUID
|
||||
err := (&uuid).Scan(stringTest)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = (&uuid).Scan([]byte(stringTest))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = (&uuid).Scan(byteTest)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// bad type tests
|
||||
|
||||
err = (&uuid).Scan(badTypeTest)
|
||||
if err == nil {
|
||||
t.Error("int correctly parsed and shouldn't have")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "unable to scan type") {
|
||||
t.Error("attempting to parse an int returned an incorrect error message")
|
||||
}
|
||||
|
||||
// invalid/incomplete uuids
|
||||
|
||||
err = (&uuid).Scan(invalidTest)
|
||||
if err == nil {
|
||||
t.Error("invalid uuid was parsed without error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "invalid UUID") {
|
||||
t.Error("attempting to parse an invalid UUID returned an incorrect error message")
|
||||
}
|
||||
|
||||
err = (&uuid).Scan(byteTest[:len(byteTest)-2])
|
||||
if err == nil {
|
||||
t.Error("invalid byte uuid was parsed without error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "invalid UUID") {
|
||||
t.Error("attempting to parse an invalid byte UUID returned an incorrect error message")
|
||||
}
|
||||
|
||||
// empty tests
|
||||
|
||||
uuid = nil
|
||||
var emptySlice []byte
|
||||
err = (&uuid).Scan(emptySlice)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if uuid != nil {
|
||||
t.Error("UUID was not nil after scanning empty byte slice")
|
||||
}
|
||||
|
||||
uuid = nil
|
||||
var emptyString string
|
||||
err = (&uuid).Scan(emptyString)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if uuid != nil {
|
||||
t.Error("UUID was not nil after scanning empty string")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValue(t *testing.T) {
|
||||
stringTest := "f47ac10b-58cc-0372-8567-0e02b2c3d479"
|
||||
uuid := Parse(stringTest)
|
||||
val, _ := uuid.Value()
|
||||
if val != stringTest {
|
||||
t.Error("Value() did not return expected string")
|
||||
}
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
// Copyright 2014 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A Time represents a time as the number of 100's of nanoseconds since 15 Oct
|
||||
// 1582.
|
||||
type Time int64
|
||||
|
||||
const (
|
||||
lillian = 2299160 // Julian day of 15 Oct 1582
|
||||
unix = 2440587 // Julian day of 1 Jan 1970
|
||||
epoch = unix - lillian // Days between epochs
|
||||
g1582 = epoch * 86400 // seconds between epochs
|
||||
g1582ns100 = g1582 * 10000000 // 100s of a nanoseconds between epochs
|
||||
)
|
||||
|
||||
var (
|
||||
timeMu sync.Mutex
|
||||
lasttime uint64 // last time we returned
|
||||
clock_seq uint16 // clock sequence for this run
|
||||
|
||||
timeNow = time.Now // for testing
|
||||
)
|
||||
|
||||
// UnixTime converts t the number of seconds and nanoseconds using the Unix
|
||||
// epoch of 1 Jan 1970.
|
||||
func (t Time) UnixTime() (sec, nsec int64) {
|
||||
sec = int64(t - g1582ns100)
|
||||
nsec = (sec % 10000000) * 100
|
||||
sec /= 10000000
|
||||
return sec, nsec
|
||||
}
|
||||
|
||||
// GetTime returns the current Time (100s of nanoseconds since 15 Oct 1582) and
|
||||
// clock sequence as well as adjusting the clock sequence as needed. An error
|
||||
// is returned if the current time cannot be determined.
|
||||
func GetTime() (Time, uint16, error) {
|
||||
defer timeMu.Unlock()
|
||||
timeMu.Lock()
|
||||
return getTime()
|
||||
}
|
||||
|
||||
func getTime() (Time, uint16, error) {
|
||||
t := timeNow()
|
||||
|
||||
// If we don't have a clock sequence already, set one.
|
||||
if clock_seq == 0 {
|
||||
setClockSequence(-1)
|
||||
}
|
||||
now := uint64(t.UnixNano()/100) + g1582ns100
|
||||
|
||||
// If time has gone backwards with this clock sequence then we
|
||||
// increment the clock sequence
|
||||
if now <= lasttime {
|
||||
clock_seq = ((clock_seq + 1) & 0x3fff) | 0x8000
|
||||
}
|
||||
lasttime = now
|
||||
return Time(now), clock_seq, nil
|
||||
}
|
||||
|
||||
// ClockSequence returns the current clock sequence, generating one if not
|
||||
// already set. The clock sequence is only used for Version 1 UUIDs.
|
||||
//
|
||||
// The uuid package does not use global static storage for the clock sequence or
|
||||
// the last time a UUID was generated. Unless SetClockSequence a new random
|
||||
// clock sequence is generated the first time a clock sequence is requested by
|
||||
// ClockSequence, GetTime, or NewUUID. (section 4.2.1.1) sequence is generated
|
||||
// for
|
||||
func ClockSequence() int {
|
||||
defer timeMu.Unlock()
|
||||
timeMu.Lock()
|
||||
return clockSequence()
|
||||
}
|
||||
|
||||
func clockSequence() int {
|
||||
if clock_seq == 0 {
|
||||
setClockSequence(-1)
|
||||
}
|
||||
return int(clock_seq & 0x3fff)
|
||||
}
|
||||
|
||||
// SetClockSeq sets the clock sequence to the lower 14 bits of seq. Setting to
|
||||
// -1 causes a new sequence to be generated.
|
||||
func SetClockSequence(seq int) {
|
||||
defer timeMu.Unlock()
|
||||
timeMu.Lock()
|
||||
setClockSequence(seq)
|
||||
}
|
||||
|
||||
func setClockSequence(seq int) {
|
||||
if seq == -1 {
|
||||
var b [2]byte
|
||||
randomBits(b[:]) // clock sequence
|
||||
seq = int(b[0])<<8 | int(b[1])
|
||||
}
|
||||
old_seq := clock_seq
|
||||
clock_seq = uint16(seq&0x3fff) | 0x8000 // Set our variant
|
||||
if old_seq != clock_seq {
|
||||
lasttime = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Time returns the time in 100s of nanoseconds since 15 Oct 1582 encoded in
|
||||
// uuid. It returns false if uuid is not valid. The time is only well defined
|
||||
// for version 1 and 2 UUIDs.
|
||||
func (uuid UUID) Time() (Time, bool) {
|
||||
if len(uuid) != 16 {
|
||||
return 0, false
|
||||
}
|
||||
time := int64(binary.BigEndian.Uint32(uuid[0:4]))
|
||||
time |= int64(binary.BigEndian.Uint16(uuid[4:6])) << 32
|
||||
time |= int64(binary.BigEndian.Uint16(uuid[6:8])&0xfff) << 48
|
||||
return Time(time), true
|
||||
}
|
||||
|
||||
// ClockSequence returns the clock sequence encoded in uuid. It returns false
|
||||
// if uuid is not valid. The clock sequence is only well defined for version 1
|
||||
// and 2 UUIDs.
|
||||
func (uuid UUID) ClockSequence() (int, bool) {
|
||||
if len(uuid) != 16 {
|
||||
return 0, false
|
||||
}
|
||||
return int(binary.BigEndian.Uint16(uuid[8:10])) & 0x3fff, true
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
// Copyright 2011 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// randomBits completely fills slice b with random data.
|
||||
func randomBits(b []byte) {
|
||||
if _, err := io.ReadFull(rander, b); err != nil {
|
||||
panic(err.Error()) // rand should never fail
|
||||
}
|
||||
}
|
||||
|
||||
// xvalues returns the value of a byte as a hexadecimal digit or 255.
|
||||
var xvalues = [256]byte{
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255, 255, 255, 255,
|
||||
255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
}
|
||||
|
||||
// xtob converts the the first two hex bytes of x into a byte.
|
||||
func xtob(x string) (byte, bool) {
|
||||
b1 := xvalues[x[0]]
|
||||
b2 := xvalues[x[1]]
|
||||
return (b1 << 4) | b2, b1 != 255 && b2 != 255
|
||||
}
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
// Copyright 2011 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Array is a pass-by-value UUID that can be used as an effecient key in a map.
|
||||
type Array [16]byte
|
||||
|
||||
// UUID converts uuid into a slice.
|
||||
func (uuid Array) UUID() UUID {
|
||||
return uuid[:]
|
||||
}
|
||||
|
||||
// String returns the string representation of uuid,
|
||||
// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.
|
||||
func (uuid Array) String() string {
|
||||
return uuid.UUID().String()
|
||||
}
|
||||
|
||||
// A UUID is a 128 bit (16 byte) Universal Unique IDentifier as defined in RFC
|
||||
// 4122.
|
||||
type UUID []byte
|
||||
|
||||
// A Version represents a UUIDs version.
|
||||
type Version byte
|
||||
|
||||
// A Variant represents a UUIDs variant.
|
||||
type Variant byte
|
||||
|
||||
// Constants returned by Variant.
|
||||
const (
|
||||
Invalid = Variant(iota) // Invalid UUID
|
||||
RFC4122 // The variant specified in RFC4122
|
||||
Reserved // Reserved, NCS backward compatibility.
|
||||
Microsoft // Reserved, Microsoft Corporation backward compatibility.
|
||||
Future // Reserved for future definition.
|
||||
)
|
||||
|
||||
var rander = rand.Reader // random function
|
||||
|
||||
// New returns a new random (version 4) UUID as a string. It is a convenience
|
||||
// function for NewRandom().String().
|
||||
func New() string {
|
||||
return NewRandom().String()
|
||||
}
|
||||
|
||||
// Parse decodes s into a UUID or returns nil. Both the UUID form of
|
||||
// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and
|
||||
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx are decoded.
|
||||
func Parse(s string) UUID {
|
||||
if len(s) == 36+9 {
|
||||
if strings.ToLower(s[:9]) != "urn:uuid:" {
|
||||
return nil
|
||||
}
|
||||
s = s[9:]
|
||||
} else if len(s) != 36 {
|
||||
return nil
|
||||
}
|
||||
if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' {
|
||||
return nil
|
||||
}
|
||||
var uuid [16]byte
|
||||
for i, x := range [16]int{
|
||||
0, 2, 4, 6,
|
||||
9, 11,
|
||||
14, 16,
|
||||
19, 21,
|
||||
24, 26, 28, 30, 32, 34} {
|
||||
if v, ok := xtob(s[x:]); !ok {
|
||||
return nil
|
||||
} else {
|
||||
uuid[i] = v
|
||||
}
|
||||
}
|
||||
return uuid[:]
|
||||
}
|
||||
|
||||
// Equal returns true if uuid1 and uuid2 are equal.
|
||||
func Equal(uuid1, uuid2 UUID) bool {
|
||||
return bytes.Equal(uuid1, uuid2)
|
||||
}
|
||||
|
||||
// Array returns an array representation of uuid that can be used as a map key.
|
||||
// Array panics if uuid is not valid.
|
||||
func (uuid UUID) Array() Array {
|
||||
if len(uuid) != 16 {
|
||||
panic("invalid uuid")
|
||||
}
|
||||
var a Array
|
||||
copy(a[:], uuid)
|
||||
return a
|
||||
}
|
||||
|
||||
// String returns the string form of uuid, xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
||||
// , or "" if uuid is invalid.
|
||||
func (uuid UUID) String() string {
|
||||
if len(uuid) != 16 {
|
||||
return ""
|
||||
}
|
||||
var buf [36]byte
|
||||
encodeHex(buf[:], uuid)
|
||||
return string(buf[:])
|
||||
}
|
||||
|
||||
// URN returns the RFC 2141 URN form of uuid,
|
||||
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, or "" if uuid is invalid.
|
||||
func (uuid UUID) URN() string {
|
||||
if len(uuid) != 16 {
|
||||
return ""
|
||||
}
|
||||
var buf [36 + 9]byte
|
||||
copy(buf[:], "urn:uuid:")
|
||||
encodeHex(buf[9:], uuid)
|
||||
return string(buf[:])
|
||||
}
|
||||
|
||||
func encodeHex(dst []byte, uuid UUID) {
|
||||
hex.Encode(dst[:], uuid[:4])
|
||||
dst[8] = '-'
|
||||
hex.Encode(dst[9:13], uuid[4:6])
|
||||
dst[13] = '-'
|
||||
hex.Encode(dst[14:18], uuid[6:8])
|
||||
dst[18] = '-'
|
||||
hex.Encode(dst[19:23], uuid[8:10])
|
||||
dst[23] = '-'
|
||||
hex.Encode(dst[24:], uuid[10:])
|
||||
}
|
||||
|
||||
// Variant returns the variant encoded in uuid. It returns Invalid if
|
||||
// uuid is invalid.
|
||||
func (uuid UUID) Variant() Variant {
|
||||
if len(uuid) != 16 {
|
||||
return Invalid
|
||||
}
|
||||
switch {
|
||||
case (uuid[8] & 0xc0) == 0x80:
|
||||
return RFC4122
|
||||
case (uuid[8] & 0xe0) == 0xc0:
|
||||
return Microsoft
|
||||
case (uuid[8] & 0xe0) == 0xe0:
|
||||
return Future
|
||||
default:
|
||||
return Reserved
|
||||
}
|
||||
}
|
||||
|
||||
// Version returns the version of uuid. It returns false if uuid is not
|
||||
// valid.
|
||||
func (uuid UUID) Version() (Version, bool) {
|
||||
if len(uuid) != 16 {
|
||||
return 0, false
|
||||
}
|
||||
return Version(uuid[6] >> 4), true
|
||||
}
|
||||
|
||||
func (v Version) String() string {
|
||||
if v > 15 {
|
||||
return fmt.Sprintf("BAD_VERSION_%d", v)
|
||||
}
|
||||
return fmt.Sprintf("VERSION_%d", v)
|
||||
}
|
||||
|
||||
func (v Variant) String() string {
|
||||
switch v {
|
||||
case RFC4122:
|
||||
return "RFC4122"
|
||||
case Reserved:
|
||||
return "Reserved"
|
||||
case Microsoft:
|
||||
return "Microsoft"
|
||||
case Future:
|
||||
return "Future"
|
||||
case Invalid:
|
||||
return "Invalid"
|
||||
}
|
||||
return fmt.Sprintf("BadVariant%d", int(v))
|
||||
}
|
||||
|
||||
// SetRand sets the random number generator to r, which implements io.Reader.
|
||||
// If r.Read returns an error when the package requests random data then
|
||||
// a panic will be issued.
|
||||
//
|
||||
// Calling SetRand with nil sets the random number generator to the default
|
||||
// generator.
|
||||
func SetRand(r io.Reader) {
|
||||
if r == nil {
|
||||
rander = rand.Reader
|
||||
return
|
||||
}
|
||||
rander = r
|
||||
}
|
||||
+543
@@ -0,0 +1,543 @@
|
||||
// Copyright 2011 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type test struct {
|
||||
in string
|
||||
version Version
|
||||
variant Variant
|
||||
isuuid bool
|
||||
}
|
||||
|
||||
var tests = []test{
|
||||
{"f47ac10b-58cc-0372-8567-0e02b2c3d479", 0, RFC4122, true},
|
||||
{"f47ac10b-58cc-1372-8567-0e02b2c3d479", 1, RFC4122, true},
|
||||
{"f47ac10b-58cc-2372-8567-0e02b2c3d479", 2, RFC4122, true},
|
||||
{"f47ac10b-58cc-3372-8567-0e02b2c3d479", 3, RFC4122, true},
|
||||
{"f47ac10b-58cc-4372-8567-0e02b2c3d479", 4, RFC4122, true},
|
||||
{"f47ac10b-58cc-5372-8567-0e02b2c3d479", 5, RFC4122, true},
|
||||
{"f47ac10b-58cc-6372-8567-0e02b2c3d479", 6, RFC4122, true},
|
||||
{"f47ac10b-58cc-7372-8567-0e02b2c3d479", 7, RFC4122, true},
|
||||
{"f47ac10b-58cc-8372-8567-0e02b2c3d479", 8, RFC4122, true},
|
||||
{"f47ac10b-58cc-9372-8567-0e02b2c3d479", 9, RFC4122, true},
|
||||
{"f47ac10b-58cc-a372-8567-0e02b2c3d479", 10, RFC4122, true},
|
||||
{"f47ac10b-58cc-b372-8567-0e02b2c3d479", 11, RFC4122, true},
|
||||
{"f47ac10b-58cc-c372-8567-0e02b2c3d479", 12, RFC4122, true},
|
||||
{"f47ac10b-58cc-d372-8567-0e02b2c3d479", 13, RFC4122, true},
|
||||
{"f47ac10b-58cc-e372-8567-0e02b2c3d479", 14, RFC4122, true},
|
||||
{"f47ac10b-58cc-f372-8567-0e02b2c3d479", 15, RFC4122, true},
|
||||
|
||||
{"urn:uuid:f47ac10b-58cc-4372-0567-0e02b2c3d479", 4, Reserved, true},
|
||||
{"URN:UUID:f47ac10b-58cc-4372-0567-0e02b2c3d479", 4, Reserved, true},
|
||||
{"f47ac10b-58cc-4372-0567-0e02b2c3d479", 4, Reserved, true},
|
||||
{"f47ac10b-58cc-4372-1567-0e02b2c3d479", 4, Reserved, true},
|
||||
{"f47ac10b-58cc-4372-2567-0e02b2c3d479", 4, Reserved, true},
|
||||
{"f47ac10b-58cc-4372-3567-0e02b2c3d479", 4, Reserved, true},
|
||||
{"f47ac10b-58cc-4372-4567-0e02b2c3d479", 4, Reserved, true},
|
||||
{"f47ac10b-58cc-4372-5567-0e02b2c3d479", 4, Reserved, true},
|
||||
{"f47ac10b-58cc-4372-6567-0e02b2c3d479", 4, Reserved, true},
|
||||
{"f47ac10b-58cc-4372-7567-0e02b2c3d479", 4, Reserved, true},
|
||||
{"f47ac10b-58cc-4372-8567-0e02b2c3d479", 4, RFC4122, true},
|
||||
{"f47ac10b-58cc-4372-9567-0e02b2c3d479", 4, RFC4122, true},
|
||||
{"f47ac10b-58cc-4372-a567-0e02b2c3d479", 4, RFC4122, true},
|
||||
{"f47ac10b-58cc-4372-b567-0e02b2c3d479", 4, RFC4122, true},
|
||||
{"f47ac10b-58cc-4372-c567-0e02b2c3d479", 4, Microsoft, true},
|
||||
{"f47ac10b-58cc-4372-d567-0e02b2c3d479", 4, Microsoft, true},
|
||||
{"f47ac10b-58cc-4372-e567-0e02b2c3d479", 4, Future, true},
|
||||
{"f47ac10b-58cc-4372-f567-0e02b2c3d479", 4, Future, true},
|
||||
|
||||
{"f47ac10b158cc-5372-a567-0e02b2c3d479", 0, Invalid, false},
|
||||
{"f47ac10b-58cc25372-a567-0e02b2c3d479", 0, Invalid, false},
|
||||
{"f47ac10b-58cc-53723a567-0e02b2c3d479", 0, Invalid, false},
|
||||
{"f47ac10b-58cc-5372-a56740e02b2c3d479", 0, Invalid, false},
|
||||
{"f47ac10b-58cc-5372-a567-0e02-2c3d479", 0, Invalid, false},
|
||||
{"g47ac10b-58cc-4372-a567-0e02b2c3d479", 0, Invalid, false},
|
||||
}
|
||||
|
||||
var constants = []struct {
|
||||
c interface{}
|
||||
name string
|
||||
}{
|
||||
{Person, "Person"},
|
||||
{Group, "Group"},
|
||||
{Org, "Org"},
|
||||
{Invalid, "Invalid"},
|
||||
{RFC4122, "RFC4122"},
|
||||
{Reserved, "Reserved"},
|
||||
{Microsoft, "Microsoft"},
|
||||
{Future, "Future"},
|
||||
{Domain(17), "Domain17"},
|
||||
{Variant(42), "BadVariant42"},
|
||||
}
|
||||
|
||||
func testTest(t *testing.T, in string, tt test) {
|
||||
uuid := Parse(in)
|
||||
if ok := (uuid != nil); ok != tt.isuuid {
|
||||
t.Errorf("Parse(%s) got %v expected %v\b", in, ok, tt.isuuid)
|
||||
}
|
||||
if uuid == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if v := uuid.Variant(); v != tt.variant {
|
||||
t.Errorf("Variant(%s) got %d expected %d\b", in, v, tt.variant)
|
||||
}
|
||||
if v, _ := uuid.Version(); v != tt.version {
|
||||
t.Errorf("Version(%s) got %d expected %d\b", in, v, tt.version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUUID(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
testTest(t, tt.in, tt)
|
||||
testTest(t, strings.ToUpper(tt.in), tt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstants(t *testing.T) {
|
||||
for x, tt := range constants {
|
||||
v, ok := tt.c.(fmt.Stringer)
|
||||
if !ok {
|
||||
t.Errorf("%x: %v: not a stringer", x, v)
|
||||
} else if s := v.String(); s != tt.name {
|
||||
v, _ := tt.c.(int)
|
||||
t.Errorf("%x: Constant %T:%d gives %q, expected %q", x, tt.c, v, s, tt.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandomUUID(t *testing.T) {
|
||||
m := make(map[string]bool)
|
||||
for x := 1; x < 32; x++ {
|
||||
uuid := NewRandom()
|
||||
s := uuid.String()
|
||||
if m[s] {
|
||||
t.Errorf("NewRandom returned duplicated UUID %s", s)
|
||||
}
|
||||
m[s] = true
|
||||
if v, _ := uuid.Version(); v != 4 {
|
||||
t.Errorf("Random UUID of version %s", v)
|
||||
}
|
||||
if uuid.Variant() != RFC4122 {
|
||||
t.Errorf("Random UUID is variant %d", uuid.Variant())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
m := make(map[string]bool)
|
||||
for x := 1; x < 32; x++ {
|
||||
s := New()
|
||||
if m[s] {
|
||||
t.Errorf("New returned duplicated UUID %s", s)
|
||||
}
|
||||
m[s] = true
|
||||
uuid := Parse(s)
|
||||
if uuid == nil {
|
||||
t.Errorf("New returned %q which does not decode", s)
|
||||
continue
|
||||
}
|
||||
if v, _ := uuid.Version(); v != 4 {
|
||||
t.Errorf("Random UUID of version %s", v)
|
||||
}
|
||||
if uuid.Variant() != RFC4122 {
|
||||
t.Errorf("Random UUID is variant %d", uuid.Variant())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func clockSeq(t *testing.T, uuid UUID) int {
|
||||
seq, ok := uuid.ClockSequence()
|
||||
if !ok {
|
||||
t.Fatalf("%s: invalid clock sequence", uuid)
|
||||
}
|
||||
return seq
|
||||
}
|
||||
|
||||
func TestClockSeq(t *testing.T) {
|
||||
// Fake time.Now for this test to return a monotonically advancing time; restore it at end.
|
||||
defer func(orig func() time.Time) { timeNow = orig }(timeNow)
|
||||
monTime := time.Now()
|
||||
timeNow = func() time.Time {
|
||||
monTime = monTime.Add(1 * time.Second)
|
||||
return monTime
|
||||
}
|
||||
|
||||
SetClockSequence(-1)
|
||||
uuid1 := NewUUID()
|
||||
uuid2 := NewUUID()
|
||||
|
||||
if clockSeq(t, uuid1) != clockSeq(t, uuid2) {
|
||||
t.Errorf("clock sequence %d != %d", clockSeq(t, uuid1), clockSeq(t, uuid2))
|
||||
}
|
||||
|
||||
SetClockSequence(-1)
|
||||
uuid2 = NewUUID()
|
||||
|
||||
// Just on the very off chance we generated the same sequence
|
||||
// two times we try again.
|
||||
if clockSeq(t, uuid1) == clockSeq(t, uuid2) {
|
||||
SetClockSequence(-1)
|
||||
uuid2 = NewUUID()
|
||||
}
|
||||
if clockSeq(t, uuid1) == clockSeq(t, uuid2) {
|
||||
t.Errorf("Duplicate clock sequence %d", clockSeq(t, uuid1))
|
||||
}
|
||||
|
||||
SetClockSequence(0x1234)
|
||||
uuid1 = NewUUID()
|
||||
if seq := clockSeq(t, uuid1); seq != 0x1234 {
|
||||
t.Errorf("%s: expected seq 0x1234 got 0x%04x", uuid1, seq)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoding(t *testing.T) {
|
||||
text := "7d444840-9dc0-11d1-b245-5ffdce74fad2"
|
||||
urn := "urn:uuid:7d444840-9dc0-11d1-b245-5ffdce74fad2"
|
||||
data := UUID{
|
||||
0x7d, 0x44, 0x48, 0x40,
|
||||
0x9d, 0xc0,
|
||||
0x11, 0xd1,
|
||||
0xb2, 0x45,
|
||||
0x5f, 0xfd, 0xce, 0x74, 0xfa, 0xd2,
|
||||
}
|
||||
if v := data.String(); v != text {
|
||||
t.Errorf("%x: encoded to %s, expected %s", data, v, text)
|
||||
}
|
||||
if v := data.URN(); v != urn {
|
||||
t.Errorf("%x: urn is %s, expected %s", data, v, urn)
|
||||
}
|
||||
|
||||
uuid := Parse(text)
|
||||
if !Equal(uuid, data) {
|
||||
t.Errorf("%s: decoded to %s, expected %s", text, uuid, data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersion1(t *testing.T) {
|
||||
uuid1 := NewUUID()
|
||||
uuid2 := NewUUID()
|
||||
|
||||
if Equal(uuid1, uuid2) {
|
||||
t.Errorf("%s:duplicate uuid", uuid1)
|
||||
}
|
||||
if v, _ := uuid1.Version(); v != 1 {
|
||||
t.Errorf("%s: version %s expected 1", uuid1, v)
|
||||
}
|
||||
if v, _ := uuid2.Version(); v != 1 {
|
||||
t.Errorf("%s: version %s expected 1", uuid2, v)
|
||||
}
|
||||
n1 := uuid1.NodeID()
|
||||
n2 := uuid2.NodeID()
|
||||
if !bytes.Equal(n1, n2) {
|
||||
t.Errorf("Different nodes %x != %x", n1, n2)
|
||||
}
|
||||
t1, ok := uuid1.Time()
|
||||
if !ok {
|
||||
t.Errorf("%s: invalid time", uuid1)
|
||||
}
|
||||
t2, ok := uuid2.Time()
|
||||
if !ok {
|
||||
t.Errorf("%s: invalid time", uuid2)
|
||||
}
|
||||
q1, ok := uuid1.ClockSequence()
|
||||
if !ok {
|
||||
t.Errorf("%s: invalid clock sequence", uuid1)
|
||||
}
|
||||
q2, ok := uuid2.ClockSequence()
|
||||
if !ok {
|
||||
t.Errorf("%s: invalid clock sequence", uuid2)
|
||||
}
|
||||
|
||||
switch {
|
||||
case t1 == t2 && q1 == q2:
|
||||
t.Error("time stopped")
|
||||
case t1 > t2 && q1 == q2:
|
||||
t.Error("time reversed")
|
||||
case t1 < t2 && q1 != q2:
|
||||
t.Error("clock sequence chaned unexpectedly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNode(t *testing.T) {
|
||||
// This test is mostly to make sure we don't leave nodeMu locked.
|
||||
ifname = ""
|
||||
if ni := NodeInterface(); ni != "" {
|
||||
t.Errorf("NodeInterface got %q, want %q", ni, "")
|
||||
}
|
||||
if SetNodeInterface("xyzzy") {
|
||||
t.Error("SetNodeInterface succeeded on a bad interface name")
|
||||
}
|
||||
if !SetNodeInterface("") {
|
||||
t.Error("SetNodeInterface failed")
|
||||
}
|
||||
if ni := NodeInterface(); ni == "" {
|
||||
t.Error("NodeInterface returned an empty string")
|
||||
}
|
||||
|
||||
ni := NodeID()
|
||||
if len(ni) != 6 {
|
||||
t.Errorf("ni got %d bytes, want 6", len(ni))
|
||||
}
|
||||
hasData := false
|
||||
for _, b := range ni {
|
||||
if b != 0 {
|
||||
hasData = true
|
||||
}
|
||||
}
|
||||
if !hasData {
|
||||
t.Error("nodeid is all zeros")
|
||||
}
|
||||
|
||||
id := []byte{1, 2, 3, 4, 5, 6, 7, 8}
|
||||
SetNodeID(id)
|
||||
ni = NodeID()
|
||||
if !bytes.Equal(ni, id[:6]) {
|
||||
t.Errorf("got nodeid %v, want %v", ni, id[:6])
|
||||
}
|
||||
|
||||
if ni := NodeInterface(); ni != "user" {
|
||||
t.Errorf("got inteface %q, want %q", ni, "user")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeAndTime(t *testing.T) {
|
||||
// Time is February 5, 1998 12:30:23.136364800 AM GMT
|
||||
|
||||
uuid := Parse("7d444840-9dc0-11d1-b245-5ffdce74fad2")
|
||||
node := []byte{0x5f, 0xfd, 0xce, 0x74, 0xfa, 0xd2}
|
||||
|
||||
ts, ok := uuid.Time()
|
||||
if ok {
|
||||
c := time.Unix(ts.UnixTime())
|
||||
want := time.Date(1998, 2, 5, 0, 30, 23, 136364800, time.UTC)
|
||||
if !c.Equal(want) {
|
||||
t.Errorf("Got time %v, want %v", c, want)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("%s: bad time", uuid)
|
||||
}
|
||||
if !bytes.Equal(node, uuid.NodeID()) {
|
||||
t.Errorf("Expected node %v got %v", node, uuid.NodeID())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMD5(t *testing.T) {
|
||||
uuid := NewMD5(NameSpace_DNS, []byte("python.org")).String()
|
||||
want := "6fa459ea-ee8a-3ca4-894e-db77e160355e"
|
||||
if uuid != want {
|
||||
t.Errorf("MD5: got %q expected %q", uuid, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSHA1(t *testing.T) {
|
||||
uuid := NewSHA1(NameSpace_DNS, []byte("python.org")).String()
|
||||
want := "886313e1-3b8a-5372-9b90-0c9aee199e5d"
|
||||
if uuid != want {
|
||||
t.Errorf("SHA1: got %q expected %q", uuid, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeID(t *testing.T) {
|
||||
nid := []byte{1, 2, 3, 4, 5, 6}
|
||||
SetNodeInterface("")
|
||||
s := NodeInterface()
|
||||
if s == "" || s == "user" {
|
||||
t.Errorf("NodeInterface %q after SetInteface", s)
|
||||
}
|
||||
node1 := NodeID()
|
||||
if node1 == nil {
|
||||
t.Error("NodeID nil after SetNodeInterface", s)
|
||||
}
|
||||
SetNodeID(nid)
|
||||
s = NodeInterface()
|
||||
if s != "user" {
|
||||
t.Errorf("Expected NodeInterface %q got %q", "user", s)
|
||||
}
|
||||
node2 := NodeID()
|
||||
if node2 == nil {
|
||||
t.Error("NodeID nil after SetNodeID", s)
|
||||
}
|
||||
if bytes.Equal(node1, node2) {
|
||||
t.Error("NodeID not changed after SetNodeID", s)
|
||||
} else if !bytes.Equal(nid, node2) {
|
||||
t.Errorf("NodeID is %x, expected %x", node2, nid)
|
||||
}
|
||||
}
|
||||
|
||||
func testDCE(t *testing.T, name string, uuid UUID, domain Domain, id uint32) {
|
||||
if uuid == nil {
|
||||
t.Errorf("%s failed", name)
|
||||
return
|
||||
}
|
||||
if v, _ := uuid.Version(); v != 2 {
|
||||
t.Errorf("%s: %s: expected version 2, got %s", name, uuid, v)
|
||||
return
|
||||
}
|
||||
if v, ok := uuid.Domain(); !ok || v != domain {
|
||||
if !ok {
|
||||
t.Errorf("%s: %d: Domain failed", name, uuid)
|
||||
} else {
|
||||
t.Errorf("%s: %s: expected domain %d, got %d", name, uuid, domain, v)
|
||||
}
|
||||
}
|
||||
if v, ok := uuid.Id(); !ok || v != id {
|
||||
if !ok {
|
||||
t.Errorf("%s: %d: Id failed", name, uuid)
|
||||
} else {
|
||||
t.Errorf("%s: %s: expected id %d, got %d", name, uuid, id, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDCE(t *testing.T) {
|
||||
testDCE(t, "NewDCESecurity", NewDCESecurity(42, 12345678), 42, 12345678)
|
||||
testDCE(t, "NewDCEPerson", NewDCEPerson(), Person, uint32(os.Getuid()))
|
||||
testDCE(t, "NewDCEGroup", NewDCEGroup(), Group, uint32(os.Getgid()))
|
||||
}
|
||||
|
||||
type badRand struct{}
|
||||
|
||||
func (r badRand) Read(buf []byte) (int, error) {
|
||||
for i, _ := range buf {
|
||||
buf[i] = byte(i)
|
||||
}
|
||||
return len(buf), nil
|
||||
}
|
||||
|
||||
func TestBadRand(t *testing.T) {
|
||||
SetRand(badRand{})
|
||||
uuid1 := New()
|
||||
uuid2 := New()
|
||||
if uuid1 != uuid2 {
|
||||
t.Errorf("expected duplicates, got %q and %q", uuid1, uuid2)
|
||||
}
|
||||
SetRand(nil)
|
||||
uuid1 = New()
|
||||
uuid2 = New()
|
||||
if uuid1 == uuid2 {
|
||||
t.Errorf("unexpected duplicates, got %q", uuid1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUUID_Array(t *testing.T) {
|
||||
expect := Array{
|
||||
0xf4, 0x7a, 0xc1, 0x0b,
|
||||
0x58, 0xcc,
|
||||
0x03, 0x72,
|
||||
0x85, 0x67,
|
||||
0x0e, 0x02, 0xb2, 0xc3, 0xd4, 0x79,
|
||||
}
|
||||
uuid := Parse("f47ac10b-58cc-0372-8567-0e02b2c3d479")
|
||||
if uuid == nil {
|
||||
t.Fatal("invalid uuid")
|
||||
}
|
||||
if uuid.Array() != expect {
|
||||
t.Fatal("invalid array")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArray_UUID(t *testing.T) {
|
||||
array := Array{
|
||||
0xf4, 0x7a, 0xc1, 0x0b,
|
||||
0x58, 0xcc,
|
||||
0x03, 0x72,
|
||||
0x85, 0x67,
|
||||
0x0e, 0x02, 0xb2, 0xc3, 0xd4, 0x79,
|
||||
}
|
||||
expect := Parse("f47ac10b-58cc-0372-8567-0e02b2c3d479")
|
||||
if expect == nil {
|
||||
t.Fatal("invalid uuid")
|
||||
}
|
||||
if !bytes.Equal(array.UUID(), expect) {
|
||||
t.Fatal("invalid uuid")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkParse(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
uuid := Parse("f47ac10b-58cc-0372-8567-0e02b2c3d479")
|
||||
if uuid == nil {
|
||||
b.Fatal("invalid uuid")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNew(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
New()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUUID_String(b *testing.B) {
|
||||
uuid := Parse("f47ac10b-58cc-0372-8567-0e02b2c3d479")
|
||||
if uuid == nil {
|
||||
b.Fatal("invalid uuid")
|
||||
}
|
||||
for i := 0; i < b.N; i++ {
|
||||
if uuid.String() == "" {
|
||||
b.Fatal("invalid uuid")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUUID_URN(b *testing.B) {
|
||||
uuid := Parse("f47ac10b-58cc-0372-8567-0e02b2c3d479")
|
||||
if uuid == nil {
|
||||
b.Fatal("invalid uuid")
|
||||
}
|
||||
for i := 0; i < b.N; i++ {
|
||||
if uuid.URN() == "" {
|
||||
b.Fatal("invalid uuid")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUUID_Array(b *testing.B) {
|
||||
expect := Array{
|
||||
0xf4, 0x7a, 0xc1, 0x0b,
|
||||
0x58, 0xcc,
|
||||
0x03, 0x72,
|
||||
0x85, 0x67,
|
||||
0x0e, 0x02, 0xb2, 0xc3, 0xd4, 0x79,
|
||||
}
|
||||
uuid := Parse("f47ac10b-58cc-0372-8567-0e02b2c3d479")
|
||||
if uuid == nil {
|
||||
b.Fatal("invalid uuid")
|
||||
}
|
||||
for i := 0; i < b.N; i++ {
|
||||
if uuid.Array() != expect {
|
||||
b.Fatal("invalid array")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkArray_UUID(b *testing.B) {
|
||||
array := Array{
|
||||
0xf4, 0x7a, 0xc1, 0x0b,
|
||||
0x58, 0xcc,
|
||||
0x03, 0x72,
|
||||
0x85, 0x67,
|
||||
0x0e, 0x02, 0xb2, 0xc3, 0xd4, 0x79,
|
||||
}
|
||||
expect := Parse("f47ac10b-58cc-0372-8567-0e02b2c3d479")
|
||||
if expect == nil {
|
||||
b.Fatal("invalid uuid")
|
||||
}
|
||||
for i := 0; i < b.N; i++ {
|
||||
if !bytes.Equal(array.UUID(), expect) {
|
||||
b.Fatal("invalid uuid")
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
// Copyright 2011 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
// NewUUID returns a Version 1 UUID based on the current NodeID and clock
|
||||
// sequence, and the current time. If the NodeID has not been set by SetNodeID
|
||||
// or SetNodeInterface then it will be set automatically. If the NodeID cannot
|
||||
// be set NewUUID returns nil. If clock sequence has not been set by
|
||||
// SetClockSequence then it will be set automatically. If GetTime fails to
|
||||
// return the current NewUUID returns nil.
|
||||
func NewUUID() UUID {
|
||||
if nodeID == nil {
|
||||
SetNodeInterface("")
|
||||
}
|
||||
|
||||
now, seq, err := GetTime()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
uuid := make([]byte, 16)
|
||||
|
||||
time_low := uint32(now & 0xffffffff)
|
||||
time_mid := uint16((now >> 32) & 0xffff)
|
||||
time_hi := uint16((now >> 48) & 0x0fff)
|
||||
time_hi |= 0x1000 // Version 1
|
||||
|
||||
binary.BigEndian.PutUint32(uuid[0:], time_low)
|
||||
binary.BigEndian.PutUint16(uuid[4:], time_mid)
|
||||
binary.BigEndian.PutUint16(uuid[6:], time_hi)
|
||||
binary.BigEndian.PutUint16(uuid[8:], seq)
|
||||
copy(uuid[10:], nodeID)
|
||||
|
||||
return uuid
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
// Copyright 2011 Google Inc. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uuid
|
||||
|
||||
// Random returns a Random (Version 4) UUID or panics.
|
||||
//
|
||||
// The strength of the UUIDs is based on the strength of the crypto/rand
|
||||
// package.
|
||||
//
|
||||
// A note about uniqueness derived from from the UUID Wikipedia entry:
|
||||
//
|
||||
// Randomly generated UUIDs have 122 random bits. One's annual risk of being
|
||||
// hit by a meteorite is estimated to be one chance in 17 billion, that
|
||||
// means the probability is about 0.00000000006 (6 × 10−11),
|
||||
// equivalent to the odds of creating a few tens of trillions of UUIDs in a
|
||||
// year and having one duplicate.
|
||||
func NewRandom() UUID {
|
||||
uuid := make([]byte, 16)
|
||||
randomBits([]byte(uuid))
|
||||
uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4
|
||||
uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10
|
||||
return uuid
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
# Created by https://www.gitignore.io
|
||||
|
||||
### OSX ###
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear on external disk
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
|
||||
### Windows ###
|
||||
# Windows image file caches
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
|
||||
# Folder config file
|
||||
Desktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
|
||||
### Linux ###
|
||||
*~
|
||||
|
||||
# KDE directory preferences
|
||||
.directory
|
||||
|
||||
|
||||
### Go ###
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
|
||||
### vim ###
|
||||
[._]*.s[a-w][a-z]
|
||||
[._]s[a-w][a-z]
|
||||
*.un~
|
||||
Session.vim
|
||||
.netrwhist
|
||||
*~
|
||||
|
||||
### JetBrains files ###
|
||||
.idea/
|
||||
*.iml
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.7.5
|
||||
- tip
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- os: osx
|
||||
go: 1.7.5
|
||||
env:
|
||||
- GOFLAGS="-tags kqueue"
|
||||
allow_failures:
|
||||
- go: tip
|
||||
|
||||
env:
|
||||
global:
|
||||
- GOBIN=$HOME/bin
|
||||
- PATH=$HOME/bin:$PATH
|
||||
|
||||
install:
|
||||
- go get -t -v ./...
|
||||
|
||||
script:
|
||||
- "(go version | grep -q 1.4) || go tool vet -all ."
|
||||
- go install $GOFLAGS ./...
|
||||
- go test -v -timeout 60s -race $GOFLAGS ./...
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
# List of individuals who contributed to the Notify package.
|
||||
#
|
||||
# The up-to-date list of the authors one may obtain with:
|
||||
#
|
||||
# ~ $ git shortlog -es | cut -f2 | rev | uniq -f1 | rev
|
||||
#
|
||||
|
||||
Pawel Blaszczyk <blaszczykpb@gmail.com>
|
||||
Pawel Knap <pawelknap88@gmail.com>
|
||||
Rafal Jeczalik <rjeczalik@gmail.com>
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014-2015 The Notify Authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
notify [](https://godoc.org/github.com/rjeczalik/notify) [](https://travis-ci.org/rjeczalik/notify "inotify + FSEvents + kqueue") [](https://ci.appveyor.com/project/rjeczalik/notify-246 "ReadDirectoryChangesW") [](https://coveralls.io/r/rjeczalik/notify?branch=master)
|
||||
======
|
||||
|
||||
Filesystem event notification library on steroids. (under active development)
|
||||
|
||||
*Documentation*
|
||||
|
||||
[godoc.org/github.com/rjeczalik/notify](https://godoc.org/github.com/rjeczalik/notify)
|
||||
|
||||
*Installation*
|
||||
|
||||
```
|
||||
~ $ go get -u github.com/rjeczalik/notify
|
||||
```
|
||||
|
||||
*Projects using notify*
|
||||
|
||||
- [github.com/rjeczalik/cmd/notify](https://godoc.org/github.com/rjeczalik/cmd/notify)
|
||||
- [github.com/cortesi/devd](https://github.com/cortesi/devd)
|
||||
- [github.com/cortesi/modd](https://github.com/cortesi/modd)
|
||||
- [github.com/syncthing/syncthing-inotify](https://github.com/syncthing/syncthing-inotify)
|
||||
- [github.com/OrlovEvgeny/TinyJPG](https://github.com/OrlovEvgeny/TinyJPG)
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
version: "{build}"
|
||||
|
||||
os: Windows Server 2012 R2
|
||||
|
||||
clone_folder: c:\projects\src\github.com\rjeczalik\notify
|
||||
|
||||
environment:
|
||||
PATH: c:\projects\bin;%PATH%
|
||||
GOPATH: c:\projects
|
||||
NOTIFY_TIMEOUT: 5s
|
||||
|
||||
install:
|
||||
- go version
|
||||
- go get -v -t ./...
|
||||
|
||||
build_script:
|
||||
- go tool vet -all .
|
||||
- go build ./...
|
||||
- go test -v -timeout 60s -race ./...
|
||||
|
||||
test: off
|
||||
|
||||
deploy: off
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var dbgprint func(...interface{})
|
||||
|
||||
var dbgprintf func(string, ...interface{})
|
||||
|
||||
var dbgcallstack func(max int) []string
|
||||
|
||||
func init() {
|
||||
if _, ok := os.LookupEnv("NOTIFY_DEBUG"); ok || debugTag {
|
||||
log.SetOutput(os.Stdout)
|
||||
log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds)
|
||||
dbgprint = func(v ...interface{}) {
|
||||
v = append([]interface{}{"[D] "}, v...)
|
||||
log.Println(v...)
|
||||
}
|
||||
dbgprintf = func(format string, v ...interface{}) {
|
||||
format = "[D] " + format
|
||||
log.Printf(format, v...)
|
||||
}
|
||||
dbgcallstack = func(max int) []string {
|
||||
pc, stack := make([]uintptr, max), make([]string, 0, max)
|
||||
runtime.Callers(2, pc)
|
||||
for _, pc := range pc {
|
||||
if f := runtime.FuncForPC(pc); f != nil {
|
||||
fname := f.Name()
|
||||
idx := strings.LastIndex(fname, string(os.PathSeparator))
|
||||
if idx != -1 {
|
||||
stack = append(stack, fname[idx+1:])
|
||||
} else {
|
||||
stack = append(stack, fname)
|
||||
}
|
||||
}
|
||||
}
|
||||
return stack
|
||||
}
|
||||
return
|
||||
}
|
||||
dbgprint = func(v ...interface{}) {}
|
||||
dbgprintf = func(format string, v ...interface{}) {}
|
||||
dbgcallstack = func(max int) []string { return nil }
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) 2014-2018 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build debug
|
||||
|
||||
package notify
|
||||
|
||||
var debugTag = true
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) 2014-2018 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build !debug
|
||||
|
||||
package notify
|
||||
|
||||
var debugTag = false
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// Package notify implements access to filesystem events.
|
||||
//
|
||||
// Notify is a high-level abstraction over filesystem watchers like inotify,
|
||||
// kqueue, FSEvents, FEN or ReadDirectoryChangesW. Watcher implementations are
|
||||
// split into two groups: ones that natively support recursive notifications
|
||||
// (FSEvents and ReadDirectoryChangesW) and ones that do not (inotify, kqueue, FEN).
|
||||
// For more details see watcher and recursiveWatcher interfaces in watcher.go
|
||||
// source file.
|
||||
//
|
||||
// On top of filesystem watchers notify maintains a watchpoint tree, which provides
|
||||
// a strategy for creating and closing filesystem watches and dispatching filesystem
|
||||
// events to user channels.
|
||||
//
|
||||
// An event set is just an event list joint using bitwise OR operator
|
||||
// into a single event value.
|
||||
// Both the platform-independent (see Constants) and specific events can be used.
|
||||
// Refer to the event_*.go source files for information about the available
|
||||
// events.
|
||||
//
|
||||
// A filesystem watch or just a watch is platform-specific entity which represents
|
||||
// a single path registered for notifications for specific event set. Setting a watch
|
||||
// means using platform-specific API calls for creating / initializing said watch.
|
||||
// For each watcher the API call is:
|
||||
//
|
||||
// - FSEvents: FSEventStreamCreate
|
||||
// - inotify: notify_add_watch
|
||||
// - kqueue: kevent
|
||||
// - ReadDirectoryChangesW: CreateFile+ReadDirectoryChangesW
|
||||
// - FEN: port_get
|
||||
//
|
||||
// To rewatch means to either shrink or expand an event set that was previously
|
||||
// registered during watch operation for particular filesystem watch.
|
||||
//
|
||||
// A watchpoint is a list of user channel and event set pairs for particular
|
||||
// path (watchpoint tree's node). A single watchpoint can contain multiple
|
||||
// different user channels registered to listen for one or more events. A single
|
||||
// user channel can be registered in one or more watchpoints, recursive and
|
||||
// non-recursive ones as well.
|
||||
package notify
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Event represents the type of filesystem action.
|
||||
//
|
||||
// Number of available event values is dependent on the target system or the
|
||||
// watcher implmenetation used (e.g. it's possible to use either kqueue or
|
||||
// FSEvents on Darwin).
|
||||
//
|
||||
// Please consult documentation for your target platform to see list of all
|
||||
// available events.
|
||||
type Event uint32
|
||||
|
||||
// Create, Remove, Write and Rename are the only event values guaranteed to be
|
||||
// present on all platforms.
|
||||
const (
|
||||
Create = osSpecificCreate
|
||||
Remove = osSpecificRemove
|
||||
Write = osSpecificWrite
|
||||
Rename = osSpecificRename
|
||||
|
||||
// All is handful alias for all platform-independent event values.
|
||||
All = Create | Remove | Write | Rename
|
||||
)
|
||||
|
||||
const internal = recursive | omit
|
||||
|
||||
// String implements fmt.Stringer interface.
|
||||
func (e Event) String() string {
|
||||
var s []string
|
||||
for _, strmap := range []map[Event]string{estr, osestr} {
|
||||
for ev, str := range strmap {
|
||||
if e&ev == ev {
|
||||
s = append(s, str)
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.Join(s, "|")
|
||||
}
|
||||
|
||||
// EventInfo describes an event reported by the underlying filesystem notification
|
||||
// subsystem.
|
||||
//
|
||||
// It always describes single event, even if the OS reported a coalesced action.
|
||||
// Reported path is absolute and clean.
|
||||
//
|
||||
// For non-recursive watchpoints its base is always equal to the path passed
|
||||
// to corresponding Watch call.
|
||||
//
|
||||
// The value of Sys if system-dependent and can be nil.
|
||||
//
|
||||
// Sys
|
||||
//
|
||||
// Under Darwin (FSEvents) Sys() always returns a non-nil *notify.FSEvent value,
|
||||
// which is defined as:
|
||||
//
|
||||
// type FSEvent struct {
|
||||
// Path string // real path of the file or directory
|
||||
// ID uint64 // ID of the event (FSEventStreamEventId)
|
||||
// Flags uint32 // joint FSEvents* flags (FSEventStreamEventFlags)
|
||||
// }
|
||||
//
|
||||
// For possible values of Flags see Darwin godoc for notify or FSEvents
|
||||
// documentation for FSEventStreamEventFlags constants:
|
||||
//
|
||||
// https://developer.apple.com/library/mac/documentation/Darwin/Reference/FSEvents_Ref/index.html#//apple_ref/doc/constant_group/FSEventStreamEventFlags
|
||||
//
|
||||
// Under Linux (inotify) Sys() always returns a non-nil *unix.InotifyEvent
|
||||
// value, defined as:
|
||||
//
|
||||
// type InotifyEvent struct {
|
||||
// Wd int32 // Watch descriptor
|
||||
// Mask uint32 // Mask describing event
|
||||
// Cookie uint32 // Unique cookie associating related events (for rename(2))
|
||||
// Len uint32 // Size of name field
|
||||
// Name [0]uint8 // Optional null-terminated name
|
||||
// }
|
||||
//
|
||||
// More information about inotify masks and the usage of inotify_event structure
|
||||
// can be found at:
|
||||
//
|
||||
// http://man7.org/linux/man-pages/man7/inotify.7.html
|
||||
//
|
||||
// Under Darwin, DragonFlyBSD, FreeBSD, NetBSD, OpenBSD (kqueue) Sys() always
|
||||
// returns a non-nil *notify.Kevent value, which is defined as:
|
||||
//
|
||||
// type Kevent struct {
|
||||
// Kevent *syscall.Kevent_t // Kevent is a kqueue specific structure
|
||||
// FI os.FileInfo // FI describes file/dir
|
||||
// }
|
||||
//
|
||||
// More information about syscall.Kevent_t can be found at:
|
||||
//
|
||||
// https://www.freebsd.org/cgi/man.cgi?query=kqueue
|
||||
//
|
||||
// Under Windows (ReadDirectoryChangesW) Sys() always returns nil. The documentation
|
||||
// of watcher's WinAPI function can be found at:
|
||||
//
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa365465%28v=vs.85%29.aspx
|
||||
type EventInfo interface {
|
||||
Event() Event // event value for the filesystem action
|
||||
Path() string // real path of the file or directory
|
||||
Sys() interface{} // underlying data source (can return nil)
|
||||
}
|
||||
|
||||
type isDirer interface {
|
||||
isDir() (bool, error)
|
||||
}
|
||||
|
||||
var _ fmt.Stringer = (*event)(nil)
|
||||
var _ isDirer = (*event)(nil)
|
||||
|
||||
// String implements fmt.Stringer interface.
|
||||
func (e *event) String() string {
|
||||
return e.Event().String() + `: "` + e.Path() + `"`
|
||||
}
|
||||
|
||||
var estr = map[Event]string{
|
||||
Create: "notify.Create",
|
||||
Remove: "notify.Remove",
|
||||
Write: "notify.Write",
|
||||
Rename: "notify.Rename",
|
||||
// Display name for recursive event is added only for debugging
|
||||
// purposes. It's an internal event after all and won't be exposed to the
|
||||
// user. Having Recursive event printable is helpful, e.g. for reading
|
||||
// testing failure messages:
|
||||
//
|
||||
// --- FAIL: TestWatchpoint (0.00 seconds)
|
||||
// watchpoint_test.go:64: want diff=[notify.Remove notify.Create|notify.Remove];
|
||||
// got [notify.Remove notify.Remove|notify.Create] (i=1)
|
||||
//
|
||||
// Yup, here the diff have Recursive event inside. Go figure.
|
||||
recursive: "recursive",
|
||||
omit: "omit",
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build solaris
|
||||
|
||||
package notify
|
||||
|
||||
const (
|
||||
osSpecificCreate Event = 0x00000100 << iota
|
||||
osSpecificRemove
|
||||
osSpecificWrite
|
||||
osSpecificRename
|
||||
// internal
|
||||
// recursive is used to distinguish recursive eventsets from non-recursive ones
|
||||
recursive
|
||||
// omit is used for dispatching internal events; only those events are sent
|
||||
// for which both the event and the watchpoint has omit in theirs event sets.
|
||||
omit
|
||||
)
|
||||
|
||||
const (
|
||||
// FileAccess is an event reported when monitored file/directory was accessed.
|
||||
FileAccess = fileAccess
|
||||
// FileModified is an event reported when monitored file/directory was modified.
|
||||
FileModified = fileModified
|
||||
// FileAttrib is an event reported when monitored file/directory's ATTRIB
|
||||
// was changed.
|
||||
FileAttrib = fileAttrib
|
||||
// FileDelete is an event reported when monitored file/directory was deleted.
|
||||
FileDelete = fileDelete
|
||||
// FileRenameTo to is an event reported when monitored file/directory was renamed.
|
||||
FileRenameTo = fileRenameTo
|
||||
// FileRenameFrom is an event reported when monitored file/directory was renamed.
|
||||
FileRenameFrom = fileRenameFrom
|
||||
// FileTrunc is an event reported when monitored file/directory was truncated.
|
||||
FileTrunc = fileTrunc
|
||||
// FileNoFollow is an flag to indicate not to follow symbolic links.
|
||||
FileNoFollow = fileNoFollow
|
||||
// Unmounted is an event reported when monitored filesystem was unmounted.
|
||||
Unmounted = unmounted
|
||||
// MountedOver is an event reported when monitored file/directory was mounted on.
|
||||
MountedOver = mountedOver
|
||||
)
|
||||
|
||||
var osestr = map[Event]string{
|
||||
FileAccess: "notify.FileAccess",
|
||||
FileModified: "notify.FileModified",
|
||||
FileAttrib: "notify.FileAttrib",
|
||||
FileDelete: "notify.FileDelete",
|
||||
FileRenameTo: "notify.FileRenameTo",
|
||||
FileRenameFrom: "notify.FileRenameFrom",
|
||||
FileTrunc: "notify.FileTrunc",
|
||||
FileNoFollow: "notify.FileNoFollow",
|
||||
Unmounted: "notify.Unmounted",
|
||||
MountedOver: "notify.MountedOver",
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build darwin,!kqueue
|
||||
|
||||
package notify
|
||||
|
||||
const (
|
||||
osSpecificCreate = Event(FSEventsCreated)
|
||||
osSpecificRemove = Event(FSEventsRemoved)
|
||||
osSpecificWrite = Event(FSEventsModified)
|
||||
osSpecificRename = Event(FSEventsRenamed)
|
||||
// internal = Event(0x100000)
|
||||
// recursive is used to distinguish recursive eventsets from non-recursive ones
|
||||
recursive = Event(0x200000)
|
||||
// omit is used for dispatching internal events; only those events are sent
|
||||
// for which both the event and the watchpoint has omit in theirs event sets.
|
||||
omit = Event(0x400000)
|
||||
)
|
||||
|
||||
// FSEvents specific event values.
|
||||
const (
|
||||
FSEventsMustScanSubDirs Event = 0x00001
|
||||
FSEventsUserDropped = 0x00002
|
||||
FSEventsKernelDropped = 0x00004
|
||||
FSEventsEventIdsWrapped = 0x00008
|
||||
FSEventsHistoryDone = 0x00010
|
||||
FSEventsRootChanged = 0x00020
|
||||
FSEventsMount = 0x00040
|
||||
FSEventsUnmount = 0x00080
|
||||
FSEventsCreated = 0x00100
|
||||
FSEventsRemoved = 0x00200
|
||||
FSEventsInodeMetaMod = 0x00400
|
||||
FSEventsRenamed = 0x00800
|
||||
FSEventsModified = 0x01000
|
||||
FSEventsFinderInfoMod = 0x02000
|
||||
FSEventsChangeOwner = 0x04000
|
||||
FSEventsXattrMod = 0x08000
|
||||
FSEventsIsFile = 0x10000
|
||||
FSEventsIsDir = 0x20000
|
||||
FSEventsIsSymlink = 0x40000
|
||||
)
|
||||
|
||||
var osestr = map[Event]string{
|
||||
FSEventsMustScanSubDirs: "notify.FSEventsMustScanSubDirs",
|
||||
FSEventsUserDropped: "notify.FSEventsUserDropped",
|
||||
FSEventsKernelDropped: "notify.FSEventsKernelDropped",
|
||||
FSEventsEventIdsWrapped: "notify.FSEventsEventIdsWrapped",
|
||||
FSEventsHistoryDone: "notify.FSEventsHistoryDone",
|
||||
FSEventsRootChanged: "notify.FSEventsRootChanged",
|
||||
FSEventsMount: "notify.FSEventsMount",
|
||||
FSEventsUnmount: "notify.FSEventsUnmount",
|
||||
FSEventsInodeMetaMod: "notify.FSEventsInodeMetaMod",
|
||||
FSEventsFinderInfoMod: "notify.FSEventsFinderInfoMod",
|
||||
FSEventsChangeOwner: "notify.FSEventsChangeOwner",
|
||||
FSEventsXattrMod: "notify.FSEventsXattrMod",
|
||||
FSEventsIsFile: "notify.FSEventsIsFile",
|
||||
FSEventsIsDir: "notify.FSEventsIsDir",
|
||||
FSEventsIsSymlink: "notify.FSEventsIsSymlink",
|
||||
}
|
||||
|
||||
type event struct {
|
||||
fse FSEvent
|
||||
event Event
|
||||
}
|
||||
|
||||
func (ei *event) Event() Event { return ei.event }
|
||||
func (ei *event) Path() string { return ei.fse.Path }
|
||||
func (ei *event) Sys() interface{} { return &ei.fse }
|
||||
func (ei *event) isDir() (bool, error) { return ei.fse.Flags&FSEventsIsDir != 0, nil }
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build linux
|
||||
|
||||
package notify
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
// Platform independent event values.
|
||||
const (
|
||||
osSpecificCreate Event = 0x100000 << iota
|
||||
osSpecificRemove
|
||||
osSpecificWrite
|
||||
osSpecificRename
|
||||
// internal
|
||||
// recursive is used to distinguish recursive eventsets from non-recursive ones
|
||||
recursive
|
||||
// omit is used for dispatching internal events; only those events are sent
|
||||
// for which both the event and the watchpoint has omit in theirs event sets.
|
||||
omit
|
||||
)
|
||||
|
||||
// Inotify specific masks are legal, implemented events that are guaranteed to
|
||||
// work with notify package on linux-based systems.
|
||||
const (
|
||||
InAccess = Event(unix.IN_ACCESS) // File was accessed
|
||||
InModify = Event(unix.IN_MODIFY) // File was modified
|
||||
InAttrib = Event(unix.IN_ATTRIB) // Metadata changed
|
||||
InCloseWrite = Event(unix.IN_CLOSE_WRITE) // Writtable file was closed
|
||||
InCloseNowrite = Event(unix.IN_CLOSE_NOWRITE) // Unwrittable file closed
|
||||
InOpen = Event(unix.IN_OPEN) // File was opened
|
||||
InMovedFrom = Event(unix.IN_MOVED_FROM) // File was moved from X
|
||||
InMovedTo = Event(unix.IN_MOVED_TO) // File was moved to Y
|
||||
InCreate = Event(unix.IN_CREATE) // Subfile was created
|
||||
InDelete = Event(unix.IN_DELETE) // Subfile was deleted
|
||||
InDeleteSelf = Event(unix.IN_DELETE_SELF) // Self was deleted
|
||||
InMoveSelf = Event(unix.IN_MOVE_SELF) // Self was moved
|
||||
)
|
||||
|
||||
var osestr = map[Event]string{
|
||||
InAccess: "notify.InAccess",
|
||||
InModify: "notify.InModify",
|
||||
InAttrib: "notify.InAttrib",
|
||||
InCloseWrite: "notify.InCloseWrite",
|
||||
InCloseNowrite: "notify.InCloseNowrite",
|
||||
InOpen: "notify.InOpen",
|
||||
InMovedFrom: "notify.InMovedFrom",
|
||||
InMovedTo: "notify.InMovedTo",
|
||||
InCreate: "notify.InCreate",
|
||||
InDelete: "notify.InDelete",
|
||||
InDeleteSelf: "notify.InDeleteSelf",
|
||||
InMoveSelf: "notify.InMoveSelf",
|
||||
}
|
||||
|
||||
// Inotify behavior events are not **currently** supported by notify package.
|
||||
const (
|
||||
inDontFollow = Event(unix.IN_DONT_FOLLOW)
|
||||
inExclUnlink = Event(unix.IN_EXCL_UNLINK)
|
||||
inMaskAdd = Event(unix.IN_MASK_ADD)
|
||||
inOneshot = Event(unix.IN_ONESHOT)
|
||||
inOnlydir = Event(unix.IN_ONLYDIR)
|
||||
)
|
||||
|
||||
type event struct {
|
||||
sys unix.InotifyEvent
|
||||
path string
|
||||
event Event
|
||||
}
|
||||
|
||||
func (e *event) Event() Event { return e.event }
|
||||
func (e *event) Path() string { return e.path }
|
||||
func (e *event) Sys() interface{} { return &e.sys }
|
||||
func (e *event) isDir() (bool, error) { return e.sys.Mask&unix.IN_ISDIR != 0, nil }
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build darwin,kqueue dragonfly freebsd netbsd openbsd
|
||||
|
||||
package notify
|
||||
|
||||
import "syscall"
|
||||
|
||||
// TODO(pblaszczyk): ensure in runtime notify built-in event values do not
|
||||
// overlap with platform-defined ones.
|
||||
|
||||
// Platform independent event values.
|
||||
const (
|
||||
osSpecificCreate Event = 0x0100 << iota
|
||||
osSpecificRemove
|
||||
osSpecificWrite
|
||||
osSpecificRename
|
||||
// internal
|
||||
// recursive is used to distinguish recursive eventsets from non-recursive ones
|
||||
recursive
|
||||
// omit is used for dispatching internal events; only those events are sent
|
||||
// for which both the event and the watchpoint has omit in theirs event sets.
|
||||
omit
|
||||
)
|
||||
|
||||
const (
|
||||
// NoteDelete is an event reported when the unlink() system call was called
|
||||
// on the file referenced by the descriptor.
|
||||
NoteDelete = Event(syscall.NOTE_DELETE)
|
||||
// NoteWrite is an event reported when a write occurred on the file
|
||||
// referenced by the descriptor.
|
||||
NoteWrite = Event(syscall.NOTE_WRITE)
|
||||
// NoteExtend is an event reported when the file referenced by the
|
||||
// descriptor was extended.
|
||||
NoteExtend = Event(syscall.NOTE_EXTEND)
|
||||
// NoteAttrib is an event reported when the file referenced
|
||||
// by the descriptor had its attributes changed.
|
||||
NoteAttrib = Event(syscall.NOTE_ATTRIB)
|
||||
// NoteLink is an event reported when the link count on the file changed.
|
||||
NoteLink = Event(syscall.NOTE_LINK)
|
||||
// NoteRename is an event reported when the file referenced
|
||||
// by the descriptor was renamed.
|
||||
NoteRename = Event(syscall.NOTE_RENAME)
|
||||
// NoteRevoke is an event reported when access to the file was revoked via
|
||||
// revoke(2) or the underlying file system was unmounted.
|
||||
NoteRevoke = Event(syscall.NOTE_REVOKE)
|
||||
)
|
||||
|
||||
var osestr = map[Event]string{
|
||||
NoteDelete: "notify.NoteDelete",
|
||||
NoteWrite: "notify.NoteWrite",
|
||||
NoteExtend: "notify.NoteExtend",
|
||||
NoteAttrib: "notify.NoteAttrib",
|
||||
NoteLink: "notify.NoteLink",
|
||||
NoteRename: "notify.NoteRename",
|
||||
NoteRevoke: "notify.NoteRevoke",
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build windows
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Platform independent event values.
|
||||
const (
|
||||
osSpecificCreate Event = 1 << (20 + iota)
|
||||
osSpecificRemove
|
||||
osSpecificWrite
|
||||
osSpecificRename
|
||||
// recursive is used to distinguish recursive eventsets from non-recursive ones
|
||||
recursive
|
||||
// omit is used for dispatching internal events; only those events are sent
|
||||
// for which both the event and the watchpoint has omit in theirs event sets.
|
||||
omit
|
||||
// dirmarker TODO(pknap)
|
||||
dirmarker
|
||||
)
|
||||
|
||||
// ReadDirectoryChangesW filters
|
||||
// On Windows the following events can be passed to Watch. A different set of
|
||||
// events (see actions below) are received on the channel passed to Watch.
|
||||
// For more information refer to
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa365465(v=vs.85).aspx
|
||||
const (
|
||||
FileNotifyChangeFileName = Event(syscall.FILE_NOTIFY_CHANGE_FILE_NAME)
|
||||
FileNotifyChangeDirName = Event(syscall.FILE_NOTIFY_CHANGE_DIR_NAME)
|
||||
FileNotifyChangeAttributes = Event(syscall.FILE_NOTIFY_CHANGE_ATTRIBUTES)
|
||||
FileNotifyChangeSize = Event(syscall.FILE_NOTIFY_CHANGE_SIZE)
|
||||
FileNotifyChangeLastWrite = Event(syscall.FILE_NOTIFY_CHANGE_LAST_WRITE)
|
||||
FileNotifyChangeLastAccess = Event(syscall.FILE_NOTIFY_CHANGE_LAST_ACCESS)
|
||||
FileNotifyChangeCreation = Event(syscall.FILE_NOTIFY_CHANGE_CREATION)
|
||||
FileNotifyChangeSecurity = Event(syscallFileNotifyChangeSecurity)
|
||||
)
|
||||
|
||||
const (
|
||||
fileNotifyChangeAll = 0x17f // logical sum of all FileNotifyChange* events.
|
||||
fileNotifyChangeModified = fileNotifyChangeAll &^ (FileNotifyChangeFileName | FileNotifyChangeDirName)
|
||||
)
|
||||
|
||||
// according to: http://msdn.microsoft.com/en-us/library/windows/desktop/aa365465(v=vs.85).aspx
|
||||
// this flag should be declared in: http://golang.org/src/pkg/syscall/ztypes_windows.go
|
||||
const syscallFileNotifyChangeSecurity = 0x00000100
|
||||
|
||||
// ReadDirectoryChangesW actions
|
||||
// The following events are returned on the channel passed to Watch, but cannot
|
||||
// be passed to Watch itself (see filters above). You can find a table showing
|
||||
// the relation between actions and filteres at
|
||||
// https://github.com/rjeczalik/notify/issues/10#issuecomment-66179535
|
||||
// The msdn documentation on actions is part of
|
||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa364391(v=vs.85).aspx
|
||||
const (
|
||||
FileActionAdded = Event(syscall.FILE_ACTION_ADDED) << 12
|
||||
FileActionRemoved = Event(syscall.FILE_ACTION_REMOVED) << 12
|
||||
FileActionModified = Event(syscall.FILE_ACTION_MODIFIED) << 14
|
||||
FileActionRenamedOldName = Event(syscall.FILE_ACTION_RENAMED_OLD_NAME) << 15
|
||||
FileActionRenamedNewName = Event(syscall.FILE_ACTION_RENAMED_NEW_NAME) << 16
|
||||
)
|
||||
|
||||
const fileActionAll = 0x7f000 // logical sum of all FileAction* events.
|
||||
|
||||
var osestr = map[Event]string{
|
||||
FileNotifyChangeFileName: "notify.FileNotifyChangeFileName",
|
||||
FileNotifyChangeDirName: "notify.FileNotifyChangeDirName",
|
||||
FileNotifyChangeAttributes: "notify.FileNotifyChangeAttributes",
|
||||
FileNotifyChangeSize: "notify.FileNotifyChangeSize",
|
||||
FileNotifyChangeLastWrite: "notify.FileNotifyChangeLastWrite",
|
||||
FileNotifyChangeLastAccess: "notify.FileNotifyChangeLastAccess",
|
||||
FileNotifyChangeCreation: "notify.FileNotifyChangeCreation",
|
||||
FileNotifyChangeSecurity: "notify.FileNotifyChangeSecurity",
|
||||
|
||||
FileActionAdded: "notify.FileActionAdded",
|
||||
FileActionRemoved: "notify.FileActionRemoved",
|
||||
FileActionModified: "notify.FileActionModified",
|
||||
FileActionRenamedOldName: "notify.FileActionRenamedOldName",
|
||||
FileActionRenamedNewName: "notify.FileActionRenamedNewName",
|
||||
}
|
||||
|
||||
const (
|
||||
fTypeUnknown uint8 = iota
|
||||
fTypeFile
|
||||
fTypeDirectory
|
||||
)
|
||||
|
||||
// TODO(ppknap) : doc.
|
||||
type event struct {
|
||||
pathw []uint16
|
||||
name string
|
||||
ftype uint8
|
||||
action uint32
|
||||
filter uint32
|
||||
e Event
|
||||
}
|
||||
|
||||
func (e *event) Event() Event { return e.e }
|
||||
func (e *event) Path() string { return filepath.Join(syscall.UTF16ToString(e.pathw), e.name) }
|
||||
func (e *event) Sys() interface{} { return e.ftype }
|
||||
|
||||
func (e *event) isDir() (bool, error) {
|
||||
if e.ftype != fTypeUnknown {
|
||||
return e.ftype == fTypeDirectory, nil
|
||||
}
|
||||
fi, err := os.Stat(e.Path())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return fi.IsDir(), nil
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build !darwin,!linux,!freebsd,!dragonfly,!netbsd,!openbsd,!windows
|
||||
// +build !kqueue,!solaris
|
||||
|
||||
package notify
|
||||
|
||||
// Platform independent event values.
|
||||
const (
|
||||
osSpecificCreate Event = 1 << iota
|
||||
osSpecificRemove
|
||||
osSpecificWrite
|
||||
osSpecificRename
|
||||
// internal
|
||||
// recursive is used to distinguish recursive eventsets from non-recursive ones
|
||||
recursive
|
||||
// omit is used for dispatching internal events; only those events are sent
|
||||
// for which both the event and the watchpoint has omit in theirs event sets.
|
||||
omit
|
||||
)
|
||||
|
||||
var osestr = map[Event]string{}
|
||||
|
||||
type event struct{}
|
||||
|
||||
func (e *event) Event() (_ Event) { return }
|
||||
func (e *event) Path() (_ string) { return }
|
||||
func (e *event) Sys() (_ interface{}) { return }
|
||||
func (e *event) isDir() (_ bool, _ error) { return }
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// S is a workaround for random event strings concatenation order.
|
||||
func s(s string) string {
|
||||
z := strings.Split(s, "|")
|
||||
sort.StringSlice(z).Sort()
|
||||
return strings.Join(z, "|")
|
||||
}
|
||||
|
||||
// This test is not safe to run in parallel with others.
|
||||
func TestEventString(t *testing.T) {
|
||||
cases := map[Event]string{
|
||||
Create: "notify.Create",
|
||||
Create | Remove: "notify.Create|notify.Remove",
|
||||
Create | Remove | Write: "notify.Create|notify.Remove|notify.Write",
|
||||
Create | Write | Rename: "notify.Create|notify.Rename|notify.Write",
|
||||
}
|
||||
for e, str := range cases {
|
||||
if s := s(e.String()); s != str {
|
||||
t.Errorf("want s=%s; got %s (e=%#x)", str, s, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build darwin,kqueue dragonfly freebsd netbsd openbsd solaris
|
||||
|
||||
package notify
|
||||
|
||||
type event struct {
|
||||
p string
|
||||
e Event
|
||||
d bool
|
||||
pe interface{}
|
||||
}
|
||||
|
||||
func (e *event) Event() Event { return e.e }
|
||||
|
||||
func (e *event) Path() string { return e.p }
|
||||
|
||||
func (e *event) Sys() interface{} { return e.pe }
|
||||
|
||||
func (e *event) isDir() (bool, error) { return e.d, nil }
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build darwin,!kqueue
|
||||
|
||||
package notify_test
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/rjeczalik/notify"
|
||||
)
|
||||
|
||||
// This example shows how to use FSEvents-specifc event values.
|
||||
func ExampleWatch_darwin() {
|
||||
// Make the channel buffered to ensure no event is dropped. Notify will drop
|
||||
// an event if the receiver is not able to keep up the sending pace.
|
||||
c := make(chan notify.EventInfo, 1)
|
||||
|
||||
// Set up a watchpoint listening for FSEvents-specific events within a
|
||||
// current working directory. Dispatch each FSEventsChangeOwner and FSEventsMount
|
||||
// events separately to c.
|
||||
if err := notify.Watch(".", c, notify.FSEventsChangeOwner, notify.FSEventsMount); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer notify.Stop(c)
|
||||
|
||||
// Block until an event is received.
|
||||
switch ei := <-c; ei.Event() {
|
||||
case notify.FSEventsChangeOwner:
|
||||
log.Println("The owner of", ei.Path(), "has changed.")
|
||||
case notify.FSEventsMount:
|
||||
log.Println("The path", ei.Path(), "has been mounted.")
|
||||
}
|
||||
}
|
||||
|
||||
// This example shows how to work with EventInfo's underlying FSEvent struct.
|
||||
// Investigating notify.(*FSEvent).Flags field we are able to say whether
|
||||
// the event's path is a file or a directory and many more.
|
||||
func ExampleWatch_darwinDirFileSymlink() {
|
||||
var must = func(err error) {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
var stop = func(c ...chan<- notify.EventInfo) {
|
||||
for _, c := range c {
|
||||
notify.Stop(c)
|
||||
}
|
||||
}
|
||||
|
||||
// Make the channels buffered to ensure no event is dropped. Notify will drop
|
||||
// an event if the receiver is not able to keep up the sending pace.
|
||||
dir := make(chan notify.EventInfo, 1)
|
||||
file := make(chan notify.EventInfo, 1)
|
||||
symlink := make(chan notify.EventInfo, 1)
|
||||
all := make(chan notify.EventInfo, 1)
|
||||
|
||||
// Set up a single watchpoint listening for FSEvents-specific events on
|
||||
// multiple user-provided channels.
|
||||
must(notify.Watch(".", dir, notify.FSEventsIsDir))
|
||||
must(notify.Watch(".", file, notify.FSEventsIsFile))
|
||||
must(notify.Watch(".", symlink, notify.FSEventsIsSymlink))
|
||||
must(notify.Watch(".", all, notify.All))
|
||||
defer stop(dir, file, symlink, all)
|
||||
|
||||
// Block until an event is received.
|
||||
select {
|
||||
case ei := <-dir:
|
||||
log.Println("The directory", ei.Path(), "has changed")
|
||||
case ei := <-file:
|
||||
log.Println("The file", ei.Path(), "has changed")
|
||||
case ei := <-symlink:
|
||||
log.Println("The symlink", ei.Path(), "has changed")
|
||||
case ei := <-all:
|
||||
var kind string
|
||||
|
||||
// Investigate underlying *notify.FSEvent struct to access more
|
||||
// information about the event.
|
||||
switch flags := ei.Sys().(*notify.FSEvent).Flags; {
|
||||
case flags¬ify.FSEventsIsFile != 0:
|
||||
kind = "file"
|
||||
case flags¬ify.FSEventsIsDir != 0:
|
||||
kind = "dir"
|
||||
case flags¬ify.FSEventsIsSymlink != 0:
|
||||
kind = "symlink"
|
||||
}
|
||||
|
||||
log.Printf("The %s under path %s has been %sd\n", kind, ei.Path(), ei.Event())
|
||||
}
|
||||
}
|
||||
|
||||
// FSEvents may report multiple filesystem actions with one, coalesced event.
|
||||
// Notify unscoalesces such event and dispatches series of single events
|
||||
// back to the user.
|
||||
//
|
||||
// This example shows how to coalesce events by investigating notify.(*FSEvent).ID
|
||||
// field, for the science.
|
||||
func ExampleWatch_darwinCoalesce() {
|
||||
// Make the channels buffered to ensure no event is dropped. Notify will drop
|
||||
// an event if the receiver is not able to keep up the sending pace.
|
||||
c := make(chan notify.EventInfo, 4)
|
||||
|
||||
// Set up a watchpoint listetning for events within current working directory.
|
||||
// Dispatch all platform-independent separately to c.
|
||||
if err := notify.Watch(".", c, notify.All); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer notify.Stop(c)
|
||||
|
||||
var id uint64
|
||||
var coalesced []notify.EventInfo
|
||||
|
||||
for ei := range c {
|
||||
switch n := ei.Sys().(*notify.FSEvent).ID; {
|
||||
case id == 0:
|
||||
id = n
|
||||
coalesced = []notify.EventInfo{ei}
|
||||
case id == n:
|
||||
coalesced = append(coalesced, ei)
|
||||
default:
|
||||
log.Printf("FSEvents reported a filesystem action with the following"+
|
||||
" coalesced events %v groupped by %d ID\n", coalesced, id)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build linux
|
||||
|
||||
package notify_test
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/rjeczalik/notify"
|
||||
)
|
||||
|
||||
// This example shows how to watch changes made on file-system by text editor
|
||||
// when saving a file. Usually, either InCloseWrite or InMovedTo (when swapping
|
||||
// with a temporary file) event is created.
|
||||
func ExampleWatch_linux() {
|
||||
// Make the channel buffered to ensure no event is dropped. Notify will drop
|
||||
// an event if the receiver is not able to keep up the sending pace.
|
||||
c := make(chan notify.EventInfo, 1)
|
||||
|
||||
// Set up a watchpoint listening for inotify-specific events within a
|
||||
// current working directory. Dispatch each InCloseWrite and InMovedTo
|
||||
// events separately to c.
|
||||
if err := notify.Watch(".", c, notify.InCloseWrite, notify.InMovedTo); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer notify.Stop(c)
|
||||
|
||||
// Block until an event is received.
|
||||
switch ei := <-c; ei.Event() {
|
||||
case notify.InCloseWrite:
|
||||
log.Println("Editing of", ei.Path(), "file is done.")
|
||||
case notify.InMovedTo:
|
||||
log.Println("File", ei.Path(), "was swapped/moved into the watched directory.")
|
||||
}
|
||||
}
|
||||
|
||||
// This example shows how to use Sys() method from EventInfo interface to tie
|
||||
// two separate events generated by rename(2) function.
|
||||
func ExampleWatch_linuxMove() {
|
||||
// Make the channel buffered to ensure no event is dropped. Notify will drop
|
||||
// an event if the receiver is not able to keep up the sending pace.
|
||||
c := make(chan notify.EventInfo, 2)
|
||||
|
||||
// Set up a watchpoint listening for inotify-specific events within a
|
||||
// current working directory. Dispatch each InMovedFrom and InMovedTo
|
||||
// events separately to c.
|
||||
if err := notify.Watch(".", c, notify.InMovedFrom, notify.InMovedTo); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer notify.Stop(c)
|
||||
|
||||
// Inotify reports move filesystem action by sending two events tied with
|
||||
// unique cookie value (uint32): one of the events is of InMovedFrom type
|
||||
// carrying move source path, while the second one is of InMoveTo type
|
||||
// carrying move destination path.
|
||||
moves := make(map[uint32]struct {
|
||||
From string
|
||||
To string
|
||||
})
|
||||
|
||||
// Wait for moves.
|
||||
for ei := range c {
|
||||
cookie := ei.Sys().(*unix.InotifyEvent).Cookie
|
||||
|
||||
info := moves[cookie]
|
||||
switch ei.Event() {
|
||||
case notify.InMovedFrom:
|
||||
info.From = ei.Path()
|
||||
case notify.InMovedTo:
|
||||
info.To = ei.Path()
|
||||
}
|
||||
moves[cookie] = info
|
||||
|
||||
if cookie != 0 && info.From != "" && info.To != "" {
|
||||
log.Println("File:", info.From, "was renamed to", info.To)
|
||||
delete(moves, cookie)
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build windows
|
||||
|
||||
package notify_test
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/rjeczalik/notify"
|
||||
)
|
||||
|
||||
// This example shows how to watch directory-name changes in the working directory subtree.
|
||||
func ExampleWatch_windows() {
|
||||
// Make the channel buffered to ensure no event is dropped. Notify will drop
|
||||
// an event if the receiver is not able to keep up the sending pace.
|
||||
c := make(chan notify.EventInfo, 4)
|
||||
|
||||
// Since notify package behaves exactly like ReadDirectoryChangesW function,
|
||||
// we must register notify.FileNotifyChangeDirName filter and wait for one
|
||||
// of FileAction* events.
|
||||
if err := notify.Watch("./...", c, notify.FileNotifyChangeDirName); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer notify.Stop(c)
|
||||
|
||||
// Wait for actions.
|
||||
for ei := range c {
|
||||
switch ei.Event() {
|
||||
case notify.FileActionAdded, notify.FileActionRenamedNewName:
|
||||
log.Println("Created:", ei.Path())
|
||||
case notify.FileActionRemoved, notify.FileActionRenamedOldName:
|
||||
log.Println("Removed:", ei.Path())
|
||||
case notify.FileActionModified:
|
||||
panic("notify: unexpected action")
|
||||
}
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package notify_test
|
||||
|
||||
import (
|
||||
"log"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/rjeczalik/notify"
|
||||
)
|
||||
|
||||
// This is a basic example showing how to work with notify.Watch function.
|
||||
func ExampleWatch() {
|
||||
// Make the channel buffered to ensure no event is dropped. Notify will drop
|
||||
// an event if the receiver is not able to keep up the sending pace.
|
||||
c := make(chan notify.EventInfo, 1)
|
||||
|
||||
// Set up a watchpoint listening on events within current working directory.
|
||||
// Dispatch each create and remove events separately to c.
|
||||
if err := notify.Watch(".", c, notify.Create, notify.Remove); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer notify.Stop(c)
|
||||
|
||||
// Block until an event is received.
|
||||
ei := <-c
|
||||
log.Println("Got event:", ei)
|
||||
}
|
||||
|
||||
// This example shows how to set up a recursive watchpoint.
|
||||
func ExampleWatch_recursive() {
|
||||
// Make the channel buffered to ensure no event is dropped. Notify will drop
|
||||
// an event if the receiver is not able to keep up the sending pace.
|
||||
c := make(chan notify.EventInfo, 1)
|
||||
|
||||
// Set up a watchpoint listening for events within a directory tree rooted
|
||||
// at current working directory. Dispatch remove events to c.
|
||||
if err := notify.Watch("./...", c, notify.Remove); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer notify.Stop(c)
|
||||
|
||||
// Block until an event is received.
|
||||
ei := <-c
|
||||
log.Println("Got event:", ei)
|
||||
}
|
||||
|
||||
// This example shows why it is important to not create leaks by stoping
|
||||
// a channel when it's no longer being used.
|
||||
func ExampleStop() {
|
||||
waitfor := func(path string, e notify.Event, timeout time.Duration) bool {
|
||||
dir, file := filepath.Split(path)
|
||||
c := make(chan notify.EventInfo, 1)
|
||||
|
||||
if err := notify.Watch(dir, c, e); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Clean up watchpoint associated with c. If Stop was not called upon
|
||||
// return the channel would be leaked as notify holds the only reference
|
||||
// to it and does not release it on its own.
|
||||
defer notify.Stop(c)
|
||||
|
||||
t := time.After(timeout)
|
||||
|
||||
for {
|
||||
select {
|
||||
case ei := <-c:
|
||||
if filepath.Base(ei.Path()) == file {
|
||||
return true
|
||||
}
|
||||
case <-t:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if waitfor("index.lock", notify.Create, 5*time.Second) {
|
||||
log.Println("The git repository was locked")
|
||||
}
|
||||
|
||||
if waitfor("index.lock", notify.Remove, 5*time.Second) {
|
||||
log.Println("The git repository was unlocked")
|
||||
}
|
||||
}
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var errSkip = errors.New("notify: skip")
|
||||
|
||||
type walkPathFunc func(nd node, isbase bool) error
|
||||
|
||||
type walkFunc func(node) error
|
||||
|
||||
func errnotexist(name string) error {
|
||||
return &os.PathError{
|
||||
Op: "Node",
|
||||
Path: name,
|
||||
Err: os.ErrNotExist,
|
||||
}
|
||||
}
|
||||
|
||||
type node struct {
|
||||
Name string
|
||||
Watch watchpoint
|
||||
Child map[string]node
|
||||
}
|
||||
|
||||
func newnode(name string) node {
|
||||
return node{
|
||||
Name: name,
|
||||
Watch: make(watchpoint),
|
||||
Child: make(map[string]node),
|
||||
}
|
||||
}
|
||||
|
||||
func (nd node) addchild(name, base string) node {
|
||||
child, ok := nd.Child[base]
|
||||
if !ok {
|
||||
child = newnode(name)
|
||||
nd.Child[base] = child
|
||||
}
|
||||
return child
|
||||
}
|
||||
|
||||
func (nd node) Add(name string) node {
|
||||
i := indexbase(nd.Name, name)
|
||||
if i == -1 {
|
||||
return node{}
|
||||
}
|
||||
for j := indexSep(name[i:]); j != -1; j = indexSep(name[i:]) {
|
||||
nd = nd.addchild(name[:i+j], name[i:i+j])
|
||||
i += j + 1
|
||||
}
|
||||
return nd.addchild(name, name[i:])
|
||||
}
|
||||
|
||||
func (nd node) AddDir(fn walkFunc) error {
|
||||
stack := []node{nd}
|
||||
Traverse:
|
||||
for n := len(stack); n != 0; n = len(stack) {
|
||||
nd, stack = stack[n-1], stack[:n-1]
|
||||
switch err := fn(nd); err {
|
||||
case nil:
|
||||
case errSkip:
|
||||
continue Traverse
|
||||
default:
|
||||
return &os.PathError{
|
||||
Op: "error while traversing",
|
||||
Path: nd.Name,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
// TODO(rjeczalik): tolerate open failures - add failed names to
|
||||
// AddDirError and notify users which names are not added to the tree.
|
||||
fi, err := ioutil.ReadDir(nd.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, fi := range fi {
|
||||
if fi.Mode()&(os.ModeSymlink|os.ModeDir) == os.ModeDir {
|
||||
name := filepath.Join(nd.Name, fi.Name())
|
||||
stack = append(stack, nd.addchild(name, name[len(nd.Name)+1:]))
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (nd node) Get(name string) (node, error) {
|
||||
i := indexbase(nd.Name, name)
|
||||
if i == -1 {
|
||||
return node{}, errnotexist(name)
|
||||
}
|
||||
ok := false
|
||||
for j := indexSep(name[i:]); j != -1; j = indexSep(name[i:]) {
|
||||
if nd, ok = nd.Child[name[i:i+j]]; !ok {
|
||||
return node{}, errnotexist(name)
|
||||
}
|
||||
i += j + 1
|
||||
}
|
||||
if nd, ok = nd.Child[name[i:]]; !ok {
|
||||
return node{}, errnotexist(name)
|
||||
}
|
||||
return nd, nil
|
||||
}
|
||||
|
||||
func (nd node) Del(name string) error {
|
||||
i := indexbase(nd.Name, name)
|
||||
if i == -1 {
|
||||
return errnotexist(name)
|
||||
}
|
||||
stack := []node{nd}
|
||||
ok := false
|
||||
for j := indexSep(name[i:]); j != -1; j = indexSep(name[i:]) {
|
||||
if nd, ok = nd.Child[name[i:i+j]]; !ok {
|
||||
return errnotexist(name[:i+j])
|
||||
}
|
||||
stack = append(stack, nd)
|
||||
}
|
||||
if nd, ok = nd.Child[name[i:]]; !ok {
|
||||
return errnotexist(name)
|
||||
}
|
||||
nd.Child = nil
|
||||
nd.Watch = nil
|
||||
for name, i = base(nd.Name), len(stack); i != 0; name, i = base(nd.Name), i-1 {
|
||||
nd = stack[i-1]
|
||||
if nd := nd.Child[name]; len(nd.Watch) > 1 || len(nd.Child) != 0 {
|
||||
break
|
||||
} else {
|
||||
nd.Child = nil
|
||||
nd.Watch = nil
|
||||
}
|
||||
delete(nd.Child, name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (nd node) Walk(fn walkFunc) error {
|
||||
stack := []node{nd}
|
||||
Traverse:
|
||||
for n := len(stack); n != 0; n = len(stack) {
|
||||
nd, stack = stack[n-1], stack[:n-1]
|
||||
switch err := fn(nd); err {
|
||||
case nil:
|
||||
case errSkip:
|
||||
continue Traverse
|
||||
default:
|
||||
return err
|
||||
}
|
||||
for name, nd := range nd.Child {
|
||||
if name == "" {
|
||||
// Node storing inactive watchpoints has empty name, skip it
|
||||
// form traversing. Root node has also an empty name, but it
|
||||
// never has a parent node.
|
||||
continue
|
||||
}
|
||||
stack = append(stack, nd)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (nd node) WalkPath(name string, fn walkPathFunc) error {
|
||||
i := indexbase(nd.Name, name)
|
||||
if i == -1 {
|
||||
return errnotexist(name)
|
||||
}
|
||||
ok := false
|
||||
for j := indexSep(name[i:]); j != -1; j = indexSep(name[i:]) {
|
||||
switch err := fn(nd, false); err {
|
||||
case nil:
|
||||
case errSkip:
|
||||
return nil
|
||||
default:
|
||||
return err
|
||||
}
|
||||
if nd, ok = nd.Child[name[i:i+j]]; !ok {
|
||||
return errnotexist(name[:i+j])
|
||||
}
|
||||
i += j + 1
|
||||
}
|
||||
switch err := fn(nd, false); err {
|
||||
case nil:
|
||||
case errSkip:
|
||||
return nil
|
||||
default:
|
||||
return err
|
||||
}
|
||||
if nd, ok = nd.Child[name[i:]]; !ok {
|
||||
return errnotexist(name)
|
||||
}
|
||||
switch err := fn(nd, true); err {
|
||||
case nil, errSkip:
|
||||
return nil
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
type root struct {
|
||||
nd node
|
||||
}
|
||||
|
||||
func (r root) addroot(name string) node {
|
||||
if vol := filepath.VolumeName(name); vol != "" {
|
||||
root, ok := r.nd.Child[vol]
|
||||
if !ok {
|
||||
root = r.nd.addchild(vol, vol)
|
||||
}
|
||||
return root
|
||||
}
|
||||
return r.nd
|
||||
}
|
||||
|
||||
func (r root) root(name string) (node, error) {
|
||||
if vol := filepath.VolumeName(name); vol != "" {
|
||||
nd, ok := r.nd.Child[vol]
|
||||
if !ok {
|
||||
return node{}, errnotexist(name)
|
||||
}
|
||||
return nd, nil
|
||||
}
|
||||
return r.nd, nil
|
||||
}
|
||||
|
||||
func (r root) Add(name string) node {
|
||||
return r.addroot(name).Add(name)
|
||||
}
|
||||
|
||||
func (r root) AddDir(dir string, fn walkFunc) error {
|
||||
return r.Add(dir).AddDir(fn)
|
||||
}
|
||||
|
||||
func (r root) Del(name string) error {
|
||||
nd, err := r.root(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nd.Del(name)
|
||||
}
|
||||
|
||||
func (r root) Get(name string) (node, error) {
|
||||
nd, err := r.root(name)
|
||||
if err != nil {
|
||||
return node{}, err
|
||||
}
|
||||
if nd.Name != name {
|
||||
if nd, err = nd.Get(name); err != nil {
|
||||
return node{}, err
|
||||
}
|
||||
}
|
||||
return nd, nil
|
||||
}
|
||||
|
||||
func (r root) Walk(name string, fn walkFunc) error {
|
||||
nd, err := r.Get(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nd.Walk(fn)
|
||||
}
|
||||
|
||||
func (r root) WalkPath(name string, fn walkPathFunc) error {
|
||||
nd, err := r.root(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nd.WalkPath(name, fn)
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// BUG(rjeczalik): Notify does not collect watchpoints, when underlying watches
|
||||
// were removed by their os-specific watcher implementations. Instead users are
|
||||
// advised to listen on persistent paths to have guarantee they receive events
|
||||
// for the whole lifetime of their applications (to discuss see #69).
|
||||
|
||||
// BUG(ppknap): Linux (inotify) does not support watcher behavior masks like
|
||||
// InOneshot, InOnlydir etc. Instead users are advised to perform the filtering
|
||||
// themselves (to discuss see #71).
|
||||
|
||||
// BUG(ppknap): Notify was not tested for short path name support under Windows
|
||||
// (ReadDirectoryChangesW).
|
||||
|
||||
// BUG(ppknap): Windows (ReadDirectoryChangesW) cannot recognize which notification
|
||||
// triggers FileActionModified event. (to discuss see #75).
|
||||
|
||||
package notify
|
||||
|
||||
var defaultTree = newTree()
|
||||
|
||||
// Watch sets up a watchpoint on path listening for events given by the events
|
||||
// argument.
|
||||
//
|
||||
// File or directory given by the path must exist, otherwise Watch will fail
|
||||
// with non-nil error. Notify resolves, for its internal purpose, any symlinks
|
||||
// the provided path may contain, so it may fail if the symlinks form a cycle.
|
||||
// It does so, since not all watcher implementations treat passed paths as-is.
|
||||
// E.g. FSEvents reports a real path for every event, setting a watchpoint
|
||||
// on /tmp will report events with paths rooted at /private/tmp etc.
|
||||
//
|
||||
// The c almost always is a buffered channel. Watch will not block sending to c
|
||||
// - the caller must ensure that c has sufficient buffer space to keep up with
|
||||
// the expected event rate.
|
||||
//
|
||||
// It is allowed to pass the same channel multiple times with different event
|
||||
// list or different paths. Calling Watch with different event lists for a single
|
||||
// watchpoint expands its event set. The only way to shrink it, is to call
|
||||
// Stop on its channel.
|
||||
//
|
||||
// Calling Watch with empty event list does expand nor shrink watchpoint's event
|
||||
// set. If c is the first channel to listen for events on the given path, Watch
|
||||
// will seamlessly create a watch on the filesystem.
|
||||
//
|
||||
// Notify dispatches copies of single filesystem event to all channels registered
|
||||
// for each path. If a single filesystem event contains multiple coalesced events,
|
||||
// each of them is dispatched separately. E.g. the following filesystem change:
|
||||
//
|
||||
// ~ $ echo Hello > Notify.txt
|
||||
//
|
||||
// dispatches two events - notify.Create and notify.Write. However, it may depend
|
||||
// on the underlying watcher implementation whether OS reports both of them.
|
||||
//
|
||||
// Windows and recursive watches
|
||||
//
|
||||
// If a directory which path was used to create recursive watch under Windows
|
||||
// gets deleted, the OS will not report such event. It is advised to keep in
|
||||
// mind this limitation while setting recursive watchpoints for your application,
|
||||
// e.g. use persistent paths like %userprofile% or watch additionally parent
|
||||
// directory of a recursive watchpoint in order to receive delete events for it.
|
||||
func Watch(path string, c chan<- EventInfo, events ...Event) error {
|
||||
return defaultTree.Watch(path, c, events...)
|
||||
}
|
||||
|
||||
// Stop removes all watchpoints registered for c. All underlying watches are
|
||||
// also removed, for which c was the last channel listening for events.
|
||||
//
|
||||
// Stop does not close c. When Stop returns, it is guaranteed that c will
|
||||
// receive no more signals.
|
||||
func Stop(c chan<- EventInfo) {
|
||||
defaultTree.Stop(c)
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build linux
|
||||
|
||||
package notify
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNotifySystemAndGlobalMix(t *testing.T) {
|
||||
n := NewNotifyTest(t, "testdata/vfs.txt")
|
||||
defer n.Close()
|
||||
|
||||
ch := NewChans(2)
|
||||
|
||||
n.Watch("src/github.com/rjeczalik/fs", ch[0], Create)
|
||||
n.Watch("src/github.com/rjeczalik/fs", ch[1], InCreate)
|
||||
|
||||
cases := []NCase{
|
||||
{
|
||||
Event: icreate(n.W(), "src/github.com/rjeczalik/fs/.main.cc.swr"),
|
||||
Receiver: Chans{ch[0], ch[1]},
|
||||
},
|
||||
}
|
||||
|
||||
n.ExpectNotifyEvents(cases, ch)
|
||||
}
|
||||
|
||||
func TestUnknownEvent(t *testing.T) {
|
||||
n := NewNotifyTest(t, "testdata/vfs.txt")
|
||||
defer n.Close()
|
||||
|
||||
ch := NewChans(1)
|
||||
|
||||
n.WatchErr("src/github.com/rjeczalik/fs", ch[0], nil, inExclUnlink)
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build windows
|
||||
|
||||
package notify
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNotifySystemSpecificEvent(t *testing.T) {
|
||||
n := NewNotifyTest(t, "testdata/vfs.txt")
|
||||
defer n.Close()
|
||||
|
||||
ch := NewChans(1)
|
||||
|
||||
n.Watch("src/github.com/rjeczalik/fs", ch[0], FileNotifyChangeFileName, FileNotifyChangeSize)
|
||||
|
||||
cases := []NCase{
|
||||
{
|
||||
Event: rremove(n.W(), "src/github.com/rjeczalik/fs/fs.go"),
|
||||
Receiver: Chans{ch[0]},
|
||||
},
|
||||
{
|
||||
Event: rwrite(n.W(), "src/github.com/rjeczalik/fs/README.md", []byte("XD")),
|
||||
Receiver: Chans{ch[0]},
|
||||
},
|
||||
}
|
||||
|
||||
n.ExpectNotifyEvents(cases, ch)
|
||||
}
|
||||
|
||||
func TestUnknownEvent(t *testing.T) {
|
||||
n := NewNotifyTest(t, "testdata/vfs.txt")
|
||||
defer n.Close()
|
||||
|
||||
ch := NewChans(1)
|
||||
|
||||
n.WatchErr("src/github.com/rjeczalik/fs", ch[0], nil, FileActionAdded)
|
||||
}
|
||||
|
||||
func TestNotifySystemAndGlobalMix(t *testing.T) {
|
||||
n := NewNotifyTest(t, "testdata/vfs.txt")
|
||||
defer n.Close()
|
||||
|
||||
ch := NewChans(3)
|
||||
|
||||
n.Watch("src/github.com/rjeczalik/fs", ch[0], Create)
|
||||
n.Watch("src/github.com/rjeczalik/fs", ch[1], FileNotifyChangeFileName)
|
||||
n.Watch("src/github.com/rjeczalik/fs", ch[2], FileNotifyChangeDirName)
|
||||
|
||||
cases := []NCase{
|
||||
{
|
||||
Event: rcreate(n.W(), "src/github.com/rjeczalik/fs/.main.cc.swr"),
|
||||
Receiver: Chans{ch[0], ch[1]},
|
||||
},
|
||||
}
|
||||
|
||||
n.ExpectNotifyEvents(cases, ch)
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build darwin linux freebsd dragonfly netbsd openbsd windows solaris
|
||||
|
||||
package notify
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNotifyExample(t *testing.T) {
|
||||
n := NewNotifyTest(t, "testdata/vfs.txt")
|
||||
defer n.Close()
|
||||
|
||||
ch := NewChans(3)
|
||||
|
||||
// Watch-points can be set explicitly via Watch/Stop calls...
|
||||
n.Watch("src/github.com/rjeczalik/fs", ch[0], Write)
|
||||
n.Watch("src/github.com/pblaszczyk/qttu", ch[0], Write)
|
||||
n.Watch("src/github.com/pblaszczyk/qttu/...", ch[1], Create)
|
||||
n.Watch("src/github.com/rjeczalik/fs/cmd/...", ch[2], Remove)
|
||||
|
||||
cases := []NCase{
|
||||
// i=0
|
||||
{
|
||||
Event: write(n.W(), "src/github.com/rjeczalik/fs/fs.go", []byte("XD")),
|
||||
Receiver: Chans{ch[0]},
|
||||
},
|
||||
// TODO(rjeczalik): #62
|
||||
// i=1
|
||||
// {
|
||||
// Event: write(n.W(), "src/github.com/pblaszczyk/qttu/README.md", []byte("XD")),
|
||||
// Receiver: Chans{ch[0]},
|
||||
// },
|
||||
// i=2
|
||||
{
|
||||
Event: write(n.W(), "src/github.com/rjeczalik/fs/cmd/gotree/go.go", []byte("XD")),
|
||||
Receiver: nil,
|
||||
},
|
||||
// i=3
|
||||
{
|
||||
Event: create(n.W(), "src/github.com/pblaszczyk/qttu/src/.main.cc.swp"),
|
||||
Receiver: Chans{ch[1]},
|
||||
},
|
||||
// i=4
|
||||
{
|
||||
Event: create(n.W(), "src/github.com/pblaszczyk/qttu/src/.main.cc.swo"),
|
||||
Receiver: Chans{ch[1]},
|
||||
},
|
||||
// i=5
|
||||
{
|
||||
Event: remove(n.W(), "src/github.com/rjeczalik/fs/cmd/gotree/go.go"),
|
||||
Receiver: Chans{ch[2]},
|
||||
},
|
||||
}
|
||||
|
||||
n.ExpectNotifyEvents(cases, ch)
|
||||
|
||||
// ...or using Call structures.
|
||||
stops := [...]Call{
|
||||
// i=0
|
||||
{
|
||||
F: FuncStop,
|
||||
C: ch[0],
|
||||
},
|
||||
// i=1
|
||||
{
|
||||
F: FuncStop,
|
||||
C: ch[1],
|
||||
},
|
||||
}
|
||||
|
||||
n.Call(stops[:]...)
|
||||
|
||||
cases = []NCase{
|
||||
// i=0
|
||||
{
|
||||
Event: write(n.W(), "src/github.com/rjeczalik/fs/fs.go", []byte("XD")),
|
||||
Receiver: nil,
|
||||
},
|
||||
// i=1
|
||||
{
|
||||
Event: write(n.W(), "src/github.com/pblaszczyk/qttu/README.md", []byte("XD")),
|
||||
Receiver: nil,
|
||||
},
|
||||
// i=2
|
||||
{
|
||||
Event: create(n.W(), "src/github.com/pblaszczyk/qttu/src/.main.cc.swr"),
|
||||
Receiver: nil,
|
||||
},
|
||||
// i=3
|
||||
{
|
||||
Event: remove(n.W(), "src/github.com/rjeczalik/fs/cmd/gotree/main.go"),
|
||||
Receiver: Chans{ch[2]},
|
||||
},
|
||||
}
|
||||
|
||||
n.ExpectNotifyEvents(cases, ch)
|
||||
}
|
||||
|
||||
func TestStop(t *testing.T) {
|
||||
t.Skip("TODO(rjeczalik)")
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build windows
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var modkernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
var procSetSystemFileCacheSize = modkernel32.NewProc("SetSystemFileCacheSize")
|
||||
var zero = uintptr(1<<(unsafe.Sizeof(uintptr(0))*8) - 1)
|
||||
|
||||
func Sync() {
|
||||
// TODO(pknap): does not work without admin privileges, but I'm going
|
||||
// to hack it.
|
||||
// r, _, err := procSetSystemFileCacheSize.Call(none, none, 0)
|
||||
// if r == 0 {
|
||||
// dbgprint("SetSystemFileCacheSize error:", err)
|
||||
// }
|
||||
}
|
||||
|
||||
// UpdateWait pauses the program for some minimal amount of time. This function
|
||||
// is required only by implementations which work asynchronously. It gives
|
||||
// watcher structure time to update its internal state.
|
||||
func UpdateWait() {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build !windows
|
||||
|
||||
package notify
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
func Sync() {
|
||||
unix.Sync()
|
||||
}
|
||||
|
||||
// UpdateWait is required only by windows watcher implementation. On other
|
||||
// platforms this function is no-op.
|
||||
func UpdateWait() {
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
src/github.com/pblaszczyk/qttu/.travis.yml
|
||||
src/github.com/pblaszczyk/qttu/include/qttu/detail/registrator.hh
|
||||
src/github.com/pblaszczyk/qttu/include/qttu/detail/registry.hh
|
||||
src/github.com/pblaszczyk/qttu/include/qttu/runner.hh
|
||||
src/github.com/pblaszczyk/qttu/LICENSE
|
||||
src/github.com/pblaszczyk/qttu/qttu.pri
|
||||
src/github.com/pblaszczyk/qttu/qttu.pro
|
||||
src/github.com/pblaszczyk/qttu/README.md
|
||||
src/github.com/pblaszczyk/qttu/src/main.cc
|
||||
src/github.com/pblaszczyk/qttu/src/reg.cc
|
||||
src/github.com/ppknap/link/.travis.yml
|
||||
src/github.com/ppknap/link/include/coost/link/definitions.hpp
|
||||
src/github.com/ppknap/link/include/coost/link/detail/bundle.hpp
|
||||
src/github.com/ppknap/link/include/coost/link/detail/container_invoker.hpp
|
||||
src/github.com/ppknap/link/include/coost/link/detail/container_value_trait.hpp
|
||||
src/github.com/ppknap/link/include/coost/link/detail/dummy_type.hpp
|
||||
src/github.com/ppknap/link/include/coost/link/detail/function_trait.hpp
|
||||
src/github.com/ppknap/link/include/coost/link/detail/immediate_invoker.hpp
|
||||
src/github.com/ppknap/link/include/coost/link/detail/stdhelpers/always_same.hpp
|
||||
src/github.com/ppknap/link/include/coost/link/detail/stdhelpers/make_unique.hpp
|
||||
src/github.com/ppknap/link/include/coost/link/detail/vertex.hpp
|
||||
src/github.com/ppknap/link/include/coost/link/detail/wire.hpp
|
||||
src/github.com/ppknap/link/include/coost/link/link.hpp
|
||||
src/github.com/ppknap/link/include/coost/link.hpp
|
||||
src/github.com/ppknap/link/Jamroot.jam
|
||||
src/github.com/ppknap/link/LICENSE.md
|
||||
src/github.com/ppknap/link/README.md
|
||||
src/github.com/ppknap/link/test/counter_helper.hpp
|
||||
src/github.com/ppknap/link/test/Jamfile.jam
|
||||
src/github.com/ppknap/link/test/test_circular_calls.cpp
|
||||
src/github.com/ppknap/link/test/test_container.cpp
|
||||
src/github.com/ppknap/link/test/test_copy.cpp
|
||||
src/github.com/ppknap/link/test/test_destructor.cpp
|
||||
src/github.com/ppknap/link/test/test_immediate.cpp
|
||||
src/github.com/ppknap/link/test/test_initialize.cpp
|
||||
src/github.com/rjeczalik/fs/.travis.yml
|
||||
src/github.com/rjeczalik/fs/appveyor.yml
|
||||
src/github.com/rjeczalik/fs/cmd/gotree/go.go
|
||||
src/github.com/rjeczalik/fs/cmd/gotree/main.go
|
||||
src/github.com/rjeczalik/fs/cmd/mktree/main.go
|
||||
src/github.com/rjeczalik/fs/fs.go
|
||||
src/github.com/rjeczalik/fs/fsutil/fixture_test.go
|
||||
src/github.com/rjeczalik/fs/fsutil/fsutil.go
|
||||
src/github.com/rjeczalik/fs/fsutil/fsutil_test.go
|
||||
src/github.com/rjeczalik/fs/fsutil/rel.go
|
||||
src/github.com/rjeczalik/fs/fsutil/rel_test.go
|
||||
src/github.com/rjeczalik/fs/fsutil/tee.go
|
||||
src/github.com/rjeczalik/fs/fsutil/tee_test.go
|
||||
src/github.com/rjeczalik/fs/LICENSE
|
||||
src/github.com/rjeczalik/fs/memfs/memfs.go
|
||||
src/github.com/rjeczalik/fs/memfs/memfs_test.go
|
||||
src/github.com/rjeczalik/fs/memfs/tree.go
|
||||
src/github.com/rjeczalik/fs/memfs/tree_test.go
|
||||
src/github.com/rjeczalik/fs/memfs/util.go
|
||||
src/github.com/rjeczalik/fs/memfs/util_test.go
|
||||
src/github.com/rjeczalik/fs/README.md
|
||||
+955
@@ -0,0 +1,955 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NOTE(rjeczalik): some useful environment variables:
|
||||
//
|
||||
// - NOTIFY_DEBUG gives some extra information about generated events
|
||||
// - NOTIFY_TIMEOUT allows for changing default wait time for watcher's
|
||||
// events
|
||||
// - NOTIFY_TMP allows for changing location of temporary directory trees
|
||||
// created for test purpose
|
||||
|
||||
var wd string
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
if wd, err = os.Getwd(); err != nil {
|
||||
panic("Getwd()=" + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func timeout() time.Duration {
|
||||
if s := os.Getenv("NOTIFY_TIMEOUT"); s != "" {
|
||||
if t, err := time.ParseDuration(s); err == nil {
|
||||
return t
|
||||
}
|
||||
}
|
||||
return 2 * time.Second
|
||||
}
|
||||
|
||||
func vfs() (string, string) {
|
||||
if s := os.Getenv("NOTIFY_TMP"); s != "" {
|
||||
return filepath.Split(s)
|
||||
}
|
||||
return "testdata", ""
|
||||
}
|
||||
|
||||
func isDir(path string) bool {
|
||||
r := path[len(path)-1]
|
||||
return r == '\\' || r == '/'
|
||||
}
|
||||
|
||||
func tmpcreateall(tmp string, path string) error {
|
||||
isdir := isDir(path)
|
||||
path = filepath.Join(tmp, filepath.FromSlash(path))
|
||||
if isdir {
|
||||
if err := os.MkdirAll(path, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := nonil(f.Sync(), f.Close()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func tmpcreate(root, path string) (bool, error) {
|
||||
isdir := isDir(path)
|
||||
path = filepath.Join(root, filepath.FromSlash(path))
|
||||
if isdir {
|
||||
if err := os.Mkdir(path, 0755); err != nil {
|
||||
return false, err
|
||||
}
|
||||
} else {
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := nonil(f.Sync(), f.Close()); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
return isdir, nil
|
||||
}
|
||||
|
||||
func tmptree(root, list string) (string, error) {
|
||||
f, err := os.Open(list)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
if root == "" {
|
||||
if root, err = ioutil.TempDir(vfs()); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
if err := tmpcreateall(root, scanner.Text()); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return root, nil
|
||||
}
|
||||
|
||||
func callern(n int) string {
|
||||
_, file, line, ok := runtime.Caller(n)
|
||||
if !ok {
|
||||
return "<unknown>"
|
||||
}
|
||||
return filepath.Base(file) + ":" + strconv.Itoa(line)
|
||||
}
|
||||
|
||||
func caller() string {
|
||||
return callern(3)
|
||||
}
|
||||
|
||||
type WCase struct {
|
||||
Action func()
|
||||
Events []EventInfo
|
||||
}
|
||||
|
||||
func (cas WCase) String() string {
|
||||
s := make([]string, 0, len(cas.Events))
|
||||
for _, ei := range cas.Events {
|
||||
s = append(s, "Event("+ei.Event().String()+")@"+filepath.FromSlash(ei.Path()))
|
||||
}
|
||||
return strings.Join(s, ", ")
|
||||
}
|
||||
|
||||
type W struct {
|
||||
Watcher watcher
|
||||
C chan EventInfo
|
||||
Timeout time.Duration
|
||||
|
||||
t *testing.T
|
||||
root string
|
||||
}
|
||||
|
||||
func newWatcherTest(t *testing.T, tree string) *W {
|
||||
root, err := tmptree("", filepath.FromSlash(tree))
|
||||
if err != nil {
|
||||
t.Fatalf(`tmptree("", %q)=%v`, tree, err)
|
||||
}
|
||||
root, _, err = cleanpath(root)
|
||||
if err != nil {
|
||||
t.Fatalf(`cleanpath(%q)=%v`, root, err)
|
||||
}
|
||||
Sync()
|
||||
return &W{
|
||||
t: t,
|
||||
root: root,
|
||||
}
|
||||
}
|
||||
|
||||
func NewWatcherTest(t *testing.T, tree string, events ...Event) *W {
|
||||
w := newWatcherTest(t, tree)
|
||||
if len(events) == 0 {
|
||||
events = []Event{Create, Remove, Write, Rename}
|
||||
}
|
||||
if rw, ok := w.watcher().(recursiveWatcher); ok {
|
||||
if err := rw.RecursiveWatch(w.root, joinevents(events)); err != nil {
|
||||
t.Fatalf("RecursiveWatch(%q, All)=%v", w.root, err)
|
||||
}
|
||||
} else {
|
||||
fn := func(path string, fi os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fi.IsDir() {
|
||||
if err := w.watcher().Watch(path, joinevents(events)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := filepath.Walk(w.root, fn); err != nil {
|
||||
t.Fatalf("Walk(%q, fn)=%v", w.root, err)
|
||||
}
|
||||
}
|
||||
drainall(w.C)
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *W) clean(path string) string {
|
||||
path, isrec, err := cleanpath(filepath.Join(w.root, path))
|
||||
if err != nil {
|
||||
w.Fatalf("cleanpath(%q)=%v", path, err)
|
||||
}
|
||||
if isrec {
|
||||
path = path + "..."
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func (w *W) Fatal(v interface{}) {
|
||||
w.t.Fatalf("%s: %v", caller(), v)
|
||||
}
|
||||
|
||||
func (w *W) Fatalf(format string, v ...interface{}) {
|
||||
w.t.Fatalf("%s: %s", caller(), fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
func (w *W) Watch(path string, e Event) {
|
||||
if err := w.watcher().Watch(w.clean(path), e); err != nil {
|
||||
w.Fatalf("Watch(%s, %v)=%v", path, e, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *W) Rewatch(path string, olde, newe Event) {
|
||||
if err := w.watcher().Rewatch(w.clean(path), olde, newe); err != nil {
|
||||
w.Fatalf("Rewatch(%s, %v, %v)=%v", path, olde, newe, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *W) Unwatch(path string) {
|
||||
if err := w.watcher().Unwatch(w.clean(path)); err != nil {
|
||||
w.Fatalf("Unwatch(%s)=%v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *W) RecursiveWatch(path string, e Event) {
|
||||
rw, ok := w.watcher().(recursiveWatcher)
|
||||
if !ok {
|
||||
w.Fatal("watcher does not implement recursive watching on this platform")
|
||||
}
|
||||
if err := rw.RecursiveWatch(w.clean(path), e); err != nil {
|
||||
w.Fatalf("RecursiveWatch(%s, %v)=%v", path, e, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *W) RecursiveRewatch(oldp, newp string, olde, newe Event) {
|
||||
rw, ok := w.watcher().(recursiveWatcher)
|
||||
if !ok {
|
||||
w.Fatal("watcher does not implement recursive watching on this platform")
|
||||
}
|
||||
if err := rw.RecursiveRewatch(w.clean(oldp), w.clean(newp), olde, newe); err != nil {
|
||||
w.Fatalf("RecursiveRewatch(%s, %s, %v, %v)=%v", oldp, newp, olde, newe, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *W) RecursiveUnwatch(path string) {
|
||||
rw, ok := w.watcher().(recursiveWatcher)
|
||||
if !ok {
|
||||
w.Fatal("watcher does not implement recursive watching on this platform")
|
||||
}
|
||||
if err := rw.RecursiveUnwatch(w.clean(path)); err != nil {
|
||||
w.Fatalf("RecursiveUnwatch(%s)=%v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *W) initwatcher(buffer int) {
|
||||
c := make(chan EventInfo, buffer)
|
||||
w.Watcher = newWatcher(c)
|
||||
w.C = c
|
||||
}
|
||||
|
||||
func (w *W) watcher() watcher {
|
||||
if w.Watcher == nil {
|
||||
w.initwatcher(512)
|
||||
}
|
||||
return w.Watcher
|
||||
}
|
||||
|
||||
func (w *W) c() chan EventInfo {
|
||||
if w.C == nil {
|
||||
w.initwatcher(512)
|
||||
}
|
||||
return w.C
|
||||
}
|
||||
|
||||
func (w *W) timeout() time.Duration {
|
||||
if w.Timeout != 0 {
|
||||
return w.Timeout
|
||||
}
|
||||
return timeout()
|
||||
}
|
||||
|
||||
func (w *W) Close() error {
|
||||
defer os.RemoveAll(w.root)
|
||||
if err := w.watcher().Close(); err != nil {
|
||||
w.Fatalf("w.Watcher.Close()=%v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func EqualEventInfo(want, got EventInfo) error {
|
||||
if got.Event() != want.Event() {
|
||||
return fmt.Errorf("want Event()=%v; got %v (path=%s)", want.Event(),
|
||||
got.Event(), want.Path())
|
||||
}
|
||||
path := strings.TrimRight(filepath.FromSlash(want.Path()), `/\`)
|
||||
if !strings.HasSuffix(got.Path(), path) {
|
||||
return fmt.Errorf("want Path()=%s; got %s (event=%v)", path, got.Path(),
|
||||
want.Event())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func HasEventInfo(want, got Event, p string) error {
|
||||
if got&want != want {
|
||||
return fmt.Errorf("want Event=%v; got %v (path=%s)", want,
|
||||
got, p)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func EqualCall(want, got Call) error {
|
||||
if want.F != got.F {
|
||||
return fmt.Errorf("want F=%v; got %v (want.P=%q, got.P=%q)", want.F, got.F, want.P, got.P)
|
||||
}
|
||||
if got.E != want.E {
|
||||
return fmt.Errorf("want E=%v; got %v (want.P=%q, got.P=%q)", want.E, got.E, want.P, got.P)
|
||||
}
|
||||
if got.NE != want.NE {
|
||||
return fmt.Errorf("want NE=%v; got %v (want.P=%q, got.P=%q)", want.NE, got.NE, want.P, got.P)
|
||||
}
|
||||
if want.C != got.C {
|
||||
return fmt.Errorf("want C=%p; got %p (want.P=%q, got.P=%q)", want.C, got.C, want.P, got.P)
|
||||
}
|
||||
if want := filepath.FromSlash(want.P); !strings.HasSuffix(got.P, want) {
|
||||
return fmt.Errorf("want P=%s; got %s", want, got.P)
|
||||
}
|
||||
if want := filepath.FromSlash(want.NP); !strings.HasSuffix(got.NP, want) {
|
||||
return fmt.Errorf("want NP=%s; got %s", want, got.NP)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func create(w *W, path string) WCase {
|
||||
return WCase{
|
||||
Action: func() {
|
||||
isdir, err := tmpcreate(w.root, filepath.FromSlash(path))
|
||||
if err != nil {
|
||||
w.Fatalf("tmpcreate(%q, %q)=%v", w.root, path, err)
|
||||
}
|
||||
if isdir {
|
||||
dbgprintf("[FS] os.Mkdir(%q)\n", path)
|
||||
} else {
|
||||
dbgprintf("[FS] os.Create(%q)\n", path)
|
||||
}
|
||||
},
|
||||
Events: []EventInfo{
|
||||
&Call{P: path, E: Create},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func remove(w *W, path string) WCase {
|
||||
return WCase{
|
||||
Action: func() {
|
||||
if err := os.RemoveAll(filepath.Join(w.root, filepath.FromSlash(path))); err != nil {
|
||||
w.Fatal(err)
|
||||
}
|
||||
dbgprintf("[FS] os.Remove(%q)\n", path)
|
||||
},
|
||||
Events: []EventInfo{
|
||||
&Call{P: path, E: Remove},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func rename(w *W, oldpath, newpath string) WCase {
|
||||
return WCase{
|
||||
Action: func() {
|
||||
err := os.Rename(filepath.Join(w.root, filepath.FromSlash(oldpath)),
|
||||
filepath.Join(w.root, filepath.FromSlash(newpath)))
|
||||
if err != nil {
|
||||
w.Fatal(err)
|
||||
}
|
||||
dbgprintf("[FS] os.Rename(%q, %q)\n", oldpath, newpath)
|
||||
},
|
||||
Events: []EventInfo{
|
||||
&Call{P: newpath, E: Rename},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func write(w *W, path string, p []byte) WCase {
|
||||
return WCase{
|
||||
Action: func() {
|
||||
f, err := os.OpenFile(filepath.Join(w.root, filepath.FromSlash(path)),
|
||||
os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
w.Fatalf("OpenFile(%q)=%v", path, err)
|
||||
}
|
||||
if _, err := f.Write(p); err != nil {
|
||||
w.Fatalf("Write(%q)=%v", path, err)
|
||||
}
|
||||
if err := nonil(f.Sync(), f.Close()); err != nil {
|
||||
w.Fatalf("Sync(%q)/Close(%q)=%v", path, path, err)
|
||||
}
|
||||
dbgprintf("[FS] Write(%q)\n", path)
|
||||
},
|
||||
Events: []EventInfo{
|
||||
&Call{P: path, E: Write},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func drainall(c chan EventInfo) (ei []EventInfo) {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
for {
|
||||
select {
|
||||
case e := <-c:
|
||||
ei = append(ei, e)
|
||||
runtime.Gosched()
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type WCaseFunc func(i int, cas WCase, ei EventInfo) error
|
||||
|
||||
func (w *W) ExpectAnyFunc(cases []WCase, fn WCaseFunc) {
|
||||
UpdateWait() // Wait some time before starting the test.
|
||||
Test:
|
||||
for i, cas := range cases {
|
||||
dbgprintf("ExpectAny: i=%d\n", i)
|
||||
cas.Action()
|
||||
Sync()
|
||||
switch cas.Events {
|
||||
case nil:
|
||||
if ei := drainall(w.C); len(ei) != 0 {
|
||||
w.Fatalf("unexpected dangling events: %v (i=%d)", ei, i)
|
||||
}
|
||||
default:
|
||||
select {
|
||||
case ei := <-w.C:
|
||||
dbgprintf("received: path=%q, event=%v, sys=%v (i=%d)", ei.Path(),
|
||||
ei.Event(), ei.Sys(), i)
|
||||
for j, want := range cas.Events {
|
||||
if err := EqualEventInfo(want, ei); err != nil {
|
||||
dbgprint(err, j)
|
||||
continue
|
||||
}
|
||||
if fn != nil {
|
||||
if err := fn(i, cas, ei); err != nil {
|
||||
w.Fatalf("ExpectAnyFunc(%d, %v)=%v", i, ei, err)
|
||||
}
|
||||
}
|
||||
drainall(w.C) // TODO(rjeczalik): revisit
|
||||
continue Test
|
||||
}
|
||||
w.Fatalf("ExpectAny received an event which does not match any of "+
|
||||
"the expected ones (i=%d): want one of %v; got %v", i, cas.Events, ei)
|
||||
case <-time.After(w.timeout()):
|
||||
w.Fatalf("timed out after %v waiting for one of %v (i=%d)", w.timeout(),
|
||||
cas.Events, i)
|
||||
}
|
||||
drainall(w.C) // TODO(rjeczalik): revisit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *W) ExpectAny(cases []WCase) {
|
||||
w.ExpectAnyFunc(cases, nil)
|
||||
}
|
||||
|
||||
func (w *W) aggregate(ei []EventInfo, pf string) (evs map[string]Event) {
|
||||
evs = make(map[string]Event)
|
||||
for _, cas := range ei {
|
||||
p := cas.Path()
|
||||
if pf != "" {
|
||||
p = filepath.Join(pf, p)
|
||||
}
|
||||
evs[p] |= cas.Event()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (w *W) ExpectAllFunc(cases []WCase) {
|
||||
UpdateWait() // Wait some time before starting the test.
|
||||
for i, cas := range cases {
|
||||
exp := w.aggregate(cas.Events, w.root)
|
||||
dbgprintf("ExpectAll: i=%d\n", i)
|
||||
cas.Action()
|
||||
Sync()
|
||||
got := w.aggregate(drainall(w.C), "")
|
||||
for ep, ee := range exp {
|
||||
ge, ok := got[ep]
|
||||
if !ok {
|
||||
w.Fatalf("missing events for %q (%v)", ep, ee)
|
||||
continue
|
||||
}
|
||||
delete(got, ep)
|
||||
if err := HasEventInfo(ee, ge, ep); err != nil {
|
||||
w.Fatalf("ExpectAll received an event which does not match "+
|
||||
"the expected ones for %q: want %v; got %v", ep, ee, ge)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if len(got) != 0 {
|
||||
w.Fatalf("ExpectAll received unexpected events: %v", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ExpectAll requires all requested events to be send.
|
||||
// It does not require events to be send in the same order or in the same
|
||||
// chunks (e.g. NoteWrite and NoteExtend reported as independent events are
|
||||
// treated the same as one NoteWrite|NoteExtend event).
|
||||
func (w *W) ExpectAll(cases []WCase) {
|
||||
w.ExpectAllFunc(cases)
|
||||
}
|
||||
|
||||
// FuncType represents enums for Watcher interface.
|
||||
type FuncType string
|
||||
|
||||
const (
|
||||
FuncWatch = FuncType("Watch")
|
||||
FuncUnwatch = FuncType("Unwatch")
|
||||
FuncRewatch = FuncType("Rewatch")
|
||||
FuncRecursiveWatch = FuncType("RecursiveWatch")
|
||||
FuncRecursiveUnwatch = FuncType("RecursiveUnwatch")
|
||||
FuncRecursiveRewatch = FuncType("RecursiveRewatch")
|
||||
FuncStop = FuncType("Stop")
|
||||
)
|
||||
|
||||
type Chans []chan EventInfo
|
||||
|
||||
func NewChans(n int) Chans {
|
||||
ch := make([]chan EventInfo, n)
|
||||
for i := range ch {
|
||||
ch[i] = make(chan EventInfo, buffer)
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
func (c Chans) Foreach(fn func(chan<- EventInfo, node)) {
|
||||
for i, ch := range c {
|
||||
fn(ch, node{Name: strconv.Itoa(i)})
|
||||
}
|
||||
}
|
||||
|
||||
func (c Chans) Drain() (ei []EventInfo) {
|
||||
n := len(c)
|
||||
stop := make(chan struct{})
|
||||
eich := make(chan EventInfo, n*buffer)
|
||||
go func() {
|
||||
defer close(eich)
|
||||
cases := make([]reflect.SelectCase, n+1)
|
||||
for i := range c {
|
||||
cases[i].Chan = reflect.ValueOf(c[i])
|
||||
cases[i].Dir = reflect.SelectRecv
|
||||
}
|
||||
cases[n].Chan = reflect.ValueOf(stop)
|
||||
cases[n].Dir = reflect.SelectRecv
|
||||
for {
|
||||
i, v, ok := reflect.Select(cases)
|
||||
if i == n {
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
panic("(Chans).Drain(): unexpected chan close")
|
||||
}
|
||||
eich <- v.Interface().(EventInfo)
|
||||
}
|
||||
}()
|
||||
<-time.After(50 * time.Duration(n) * time.Millisecond)
|
||||
close(stop)
|
||||
for e := range eich {
|
||||
ei = append(ei, e)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Call represents single call to Watcher issued by the Tree
|
||||
// and recorded by a spy Watcher mock.
|
||||
type Call struct {
|
||||
F FuncType // denotes type of function to call, for both watcher and notifier interface
|
||||
C chan EventInfo // user channel being an argument to either Watch or Stop function
|
||||
P string // regular Path argument and old path from RecursiveRewatch call
|
||||
NP string // new Path argument from RecursiveRewatch call
|
||||
E Event // regular Event argument and old Event from a Rewatch call
|
||||
NE Event // new Event argument from Rewatch call
|
||||
S interface{} // when Call is used as EventInfo, S is a value of Sys()
|
||||
Dir bool // when Call is used as EventInfo, Dir is a value of isDir()
|
||||
}
|
||||
|
||||
// Call implements the EventInfo interface.
|
||||
func (c *Call) Event() Event { return c.E }
|
||||
func (c *Call) Path() string { return c.P }
|
||||
func (c *Call) String() string { return fmt.Sprintf("%#v", c) }
|
||||
func (c *Call) Sys() interface{} { return c.S }
|
||||
func (c *Call) isDir() (bool, error) { return c.Dir, nil }
|
||||
|
||||
// CallSlice is a convenient wrapper for a slice of Call values, which allows
|
||||
// to sort them in ascending order.
|
||||
type CallSlice []Call
|
||||
|
||||
// CallSlice implements sort.Interface inteface.
|
||||
func (cs CallSlice) Len() int { return len(cs) }
|
||||
func (cs CallSlice) Less(i, j int) bool { return cs[i].P < cs[j].P }
|
||||
func (cs CallSlice) Swap(i, j int) { cs[i], cs[j] = cs[j], cs[i] }
|
||||
func (cs CallSlice) Sort() { sort.Sort(cs) }
|
||||
|
||||
// Spy is a mock for Watcher interface, which records every call.
|
||||
type Spy []Call
|
||||
|
||||
func (s *Spy) Close() (_ error) { return }
|
||||
|
||||
func (s *Spy) Watch(p string, e Event) (_ error) {
|
||||
dbgprintf("%s: (*Spy).Watch(%q, %v)", caller(), p, e)
|
||||
*s = append(*s, Call{F: FuncWatch, P: p, E: e})
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Spy) Unwatch(p string) (_ error) {
|
||||
dbgprintf("%s: (*Spy).Unwatch(%q)", caller(), p)
|
||||
*s = append(*s, Call{F: FuncUnwatch, P: p})
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Spy) Rewatch(p string, olde, newe Event) (_ error) {
|
||||
dbgprintf("%s: (*Spy).Rewatch(%q, %v, %v)", caller(), p, olde, newe)
|
||||
*s = append(*s, Call{F: FuncRewatch, P: p, E: olde, NE: newe})
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Spy) RecursiveWatch(p string, e Event) (_ error) {
|
||||
dbgprintf("%s: (*Spy).RecursiveWatch(%q, %v)", caller(), p, e)
|
||||
*s = append(*s, Call{F: FuncRecursiveWatch, P: p, E: e})
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Spy) RecursiveUnwatch(p string) (_ error) {
|
||||
dbgprintf("%s: (*Spy).RecursiveUnwatch(%q)", caller(), p)
|
||||
*s = append(*s, Call{F: FuncRecursiveUnwatch, P: p})
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Spy) RecursiveRewatch(oldp, newp string, olde, newe Event) (_ error) {
|
||||
dbgprintf("%s: (*Spy).RecursiveRewatch(%q, %q, %v, %v)", caller(), oldp, newp, olde, newe)
|
||||
*s = append(*s, Call{F: FuncRecursiveRewatch, P: oldp, NP: newp, E: olde, NE: newe})
|
||||
return
|
||||
}
|
||||
|
||||
type RCase struct {
|
||||
Call Call
|
||||
Record []Call
|
||||
}
|
||||
|
||||
type TCase struct {
|
||||
Event Call
|
||||
Receiver Chans
|
||||
}
|
||||
|
||||
type NCase struct {
|
||||
Event WCase
|
||||
Receiver Chans
|
||||
}
|
||||
|
||||
type N struct {
|
||||
Timeout time.Duration
|
||||
|
||||
t *testing.T
|
||||
tree tree
|
||||
w *W
|
||||
spy *Spy
|
||||
c chan EventInfo
|
||||
j int // spy offset
|
||||
|
||||
realroot string
|
||||
}
|
||||
|
||||
func newN(t *testing.T, tree string) *N {
|
||||
n := &N{
|
||||
t: t,
|
||||
w: newWatcherTest(t, tree),
|
||||
}
|
||||
realroot, err := canonical(n.w.root)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: unexpected fixture failure: %v", caller(), err)
|
||||
}
|
||||
n.realroot = realroot
|
||||
return n
|
||||
}
|
||||
|
||||
func newTreeN(t *testing.T, tree string) *N {
|
||||
c := make(chan EventInfo, buffer)
|
||||
n := newN(t, tree)
|
||||
n.spy = &Spy{}
|
||||
n.w.Watcher = n.spy
|
||||
n.w.C = c
|
||||
n.c = c
|
||||
return n
|
||||
}
|
||||
|
||||
func NewNotifyTest(t *testing.T, tree string) *N {
|
||||
n := newN(t, tree)
|
||||
if rw, ok := n.w.watcher().(recursiveWatcher); ok {
|
||||
n.tree = newRecursiveTree(rw, n.w.c())
|
||||
} else {
|
||||
n.tree = newNonrecursiveTree(n.w.watcher(), n.w.c(), nil)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func NewRecursiveTreeTest(t *testing.T, tree string) *N {
|
||||
n := newTreeN(t, tree)
|
||||
n.tree = newRecursiveTree(n.spy, n.c)
|
||||
return n
|
||||
}
|
||||
|
||||
func NewNonrecursiveTreeTest(t *testing.T, tree string) *N {
|
||||
n := newTreeN(t, tree)
|
||||
n.tree = newNonrecursiveTree(n.spy, n.c, nil)
|
||||
return n
|
||||
}
|
||||
|
||||
func NewNonrecursiveTreeTestC(t *testing.T, tree string) (*N, chan EventInfo) {
|
||||
rec := make(chan EventInfo, buffer)
|
||||
recinternal := make(chan EventInfo, buffer)
|
||||
recuser := make(chan EventInfo, buffer)
|
||||
go func() {
|
||||
for ei := range rec {
|
||||
select {
|
||||
case recinternal <- ei:
|
||||
default:
|
||||
t.Fatalf("failed to send ei to recinternal: not ready")
|
||||
}
|
||||
select {
|
||||
case recuser <- ei:
|
||||
default:
|
||||
t.Fatalf("failed to send ei to recuser: not ready")
|
||||
}
|
||||
}
|
||||
}()
|
||||
n := newTreeN(t, tree)
|
||||
tr := newNonrecursiveTree(n.spy, n.c, recinternal)
|
||||
tr.rec = rec
|
||||
n.tree = tr
|
||||
return n, recuser
|
||||
}
|
||||
|
||||
func (n *N) timeout() time.Duration {
|
||||
if n.Timeout != 0 {
|
||||
return n.Timeout
|
||||
}
|
||||
return n.w.timeout()
|
||||
}
|
||||
|
||||
func (n *N) W() *W {
|
||||
return n.w
|
||||
}
|
||||
|
||||
func (n *N) Close() error {
|
||||
defer os.RemoveAll(n.w.root)
|
||||
if err := n.tree.Close(); err != nil {
|
||||
n.w.Fatalf("(notifier).Close()=%v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *N) Watch(path string, c chan<- EventInfo, events ...Event) {
|
||||
UpdateWait() // we need to wait on Windows because of its asynchronous watcher.
|
||||
path = filepath.Join(n.w.root, path)
|
||||
if err := n.tree.Watch(path, c, events...); err != nil {
|
||||
n.t.Errorf("Watch(%s, %p, %v)=%v", path, c, events, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *N) WatchErr(path string, c chan<- EventInfo, err error, events ...Event) {
|
||||
path = filepath.Join(n.w.root, path)
|
||||
switch e := n.tree.Watch(path, c, events...); {
|
||||
case err == nil && e == nil:
|
||||
n.t.Errorf("Watch(%s, %p, %v)=nil", path, c, events)
|
||||
case err != nil && e != err:
|
||||
n.t.Errorf("Watch(%s, %p, %v)=%v != %v", path, c, events, e, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *N) Stop(c chan<- EventInfo) {
|
||||
n.tree.Stop(c)
|
||||
}
|
||||
|
||||
func (n *N) Call(calls ...Call) {
|
||||
for i := range calls {
|
||||
switch calls[i].F {
|
||||
case FuncWatch:
|
||||
n.Watch(calls[i].P, calls[i].C, calls[i].E)
|
||||
case FuncStop:
|
||||
n.Stop(calls[i].C)
|
||||
default:
|
||||
panic("unsupported call type: " + string(calls[i].F))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (n *N) expectDry(ch Chans, i int) {
|
||||
if ei := ch.Drain(); len(ei) != 0 {
|
||||
n.w.Fatalf("unexpected dangling events: %v (i=%d)", ei, i)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *N) ExpectRecordedCalls(cases []RCase) {
|
||||
for i, cas := range cases {
|
||||
dbgprintf("ExpectRecordedCalls: i=%d\n", i)
|
||||
n.Call(cas.Call)
|
||||
record := (*n.spy)[n.j:]
|
||||
if len(cas.Record) == 0 && len(record) == 0 {
|
||||
continue
|
||||
}
|
||||
n.j = len(*n.spy)
|
||||
if len(record) != len(cas.Record) {
|
||||
n.t.Fatalf("%s: want len(record)=%d; got %d [%+v] (i=%d)", caller(),
|
||||
len(cas.Record), len(record), record, i)
|
||||
}
|
||||
CallSlice(record).Sort()
|
||||
for j := range cas.Record {
|
||||
if err := EqualCall(cas.Record[j], record[j]); err != nil {
|
||||
n.t.Fatalf("%s: %v (i=%d, j=%d)", caller(), err, i, j)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (n *N) collect(ch Chans) <-chan []EventInfo {
|
||||
done := make(chan []EventInfo)
|
||||
go func() {
|
||||
cases := make([]reflect.SelectCase, len(ch))
|
||||
unique := make(map[<-chan EventInfo]EventInfo, len(ch))
|
||||
for i := range ch {
|
||||
cases[i].Chan = reflect.ValueOf(ch[i])
|
||||
cases[i].Dir = reflect.SelectRecv
|
||||
}
|
||||
for i := len(cases); i != 0; i = len(cases) {
|
||||
j, v, ok := reflect.Select(cases)
|
||||
if !ok {
|
||||
n.t.Fatal("unexpected chan close")
|
||||
}
|
||||
ch := cases[j].Chan.Interface().(chan EventInfo)
|
||||
got := v.Interface().(EventInfo)
|
||||
if ei, ok := unique[ch]; ok {
|
||||
n.t.Fatalf("duplicated event %v (previous=%v) received on collect", got, ei)
|
||||
}
|
||||
unique[ch] = got
|
||||
cases[j], cases = cases[i-1], cases[:i-1]
|
||||
}
|
||||
collected := make([]EventInfo, 0, len(ch))
|
||||
for _, ch := range unique {
|
||||
collected = append(collected, ch)
|
||||
}
|
||||
done <- collected
|
||||
}()
|
||||
return done
|
||||
}
|
||||
|
||||
func (n *N) abs(rel Call) *Call {
|
||||
rel.P = filepath.Join(n.realroot, filepath.FromSlash(rel.P))
|
||||
if !filepath.IsAbs(rel.P) {
|
||||
rel.P = filepath.Join(wd, rel.P)
|
||||
}
|
||||
return &rel
|
||||
}
|
||||
|
||||
func (n *N) ExpectTreeEvents(cases []TCase, all Chans) {
|
||||
for i, cas := range cases {
|
||||
dbgprintf("ExpectTreeEvents: i=%d\n", i)
|
||||
// Ensure there're no dangling event left by previous test-case.
|
||||
n.expectDry(all, i)
|
||||
n.c <- n.abs(cas.Event)
|
||||
switch cas.Receiver {
|
||||
case nil:
|
||||
n.expectDry(all, i)
|
||||
default:
|
||||
ch := n.collect(cas.Receiver)
|
||||
select {
|
||||
case collected := <-ch:
|
||||
for _, got := range collected {
|
||||
if err := EqualEventInfo(&cas.Event, got); err != nil {
|
||||
n.w.Fatalf("%s: %s (i=%d)", caller(), err, i)
|
||||
}
|
||||
}
|
||||
case <-time.After(n.timeout()):
|
||||
n.w.Fatalf("ExpectTreeEvents has timed out after %v waiting for"+
|
||||
" %v on %s (i=%d)", n.timeout(), cas.Event.E, cas.Event.P, i)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
n.expectDry(all, -1)
|
||||
}
|
||||
|
||||
func (n *N) ExpectNotifyEvents(cases []NCase, all Chans) {
|
||||
UpdateWait() // Wait some time before starting the test.
|
||||
for i, cas := range cases {
|
||||
dbgprintf("ExpectNotifyEvents: i=%d\n", i)
|
||||
cas.Event.Action()
|
||||
Sync()
|
||||
switch cas.Receiver {
|
||||
case nil:
|
||||
n.expectDry(all, i)
|
||||
default:
|
||||
ch := n.collect(cas.Receiver)
|
||||
select {
|
||||
case collected := <-ch:
|
||||
Compare:
|
||||
for j, ei := range collected {
|
||||
dbgprintf("received: path=%q, event=%v, sys=%v (i=%d, j=%d)", ei.Path(),
|
||||
ei.Event(), ei.Sys(), i, j)
|
||||
for _, want := range cas.Event.Events {
|
||||
if err := EqualEventInfo(want, ei); err != nil {
|
||||
dbgprint(err, j)
|
||||
continue
|
||||
}
|
||||
continue Compare
|
||||
}
|
||||
n.w.Fatalf("ExpectNotifyEvents received an event which does not"+
|
||||
" match any of the expected ones (i=%d): want one of %v; got %v", i,
|
||||
cas.Event.Events, ei)
|
||||
}
|
||||
case <-time.After(n.timeout()):
|
||||
n.w.Fatalf("ExpectNotifyEvents did not receive any of the expected events [%v] "+
|
||||
"after %v (i=%d)", cas.Event, n.timeout(), i)
|
||||
}
|
||||
}
|
||||
}
|
||||
n.expectDry(all, -1)
|
||||
}
|
||||
|
||||
func (n *N) Walk(fn walkFunc) {
|
||||
switch t := n.tree.(type) {
|
||||
case *recursiveTree:
|
||||
if err := t.root.Walk("", fn); err != nil {
|
||||
n.w.Fatal(err)
|
||||
}
|
||||
case *nonrecursiveTree:
|
||||
if err := t.root.Walk("", fn); err != nil {
|
||||
n.w.Fatal(err)
|
||||
}
|
||||
default:
|
||||
n.t.Fatal("unknown tree type")
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package notify
|
||||
|
||||
const buffer = 128
|
||||
|
||||
type tree interface {
|
||||
Watch(string, chan<- EventInfo, ...Event) error
|
||||
Stop(chan<- EventInfo)
|
||||
Close() error
|
||||
}
|
||||
|
||||
func newTree() tree {
|
||||
c := make(chan EventInfo, buffer)
|
||||
w := newWatcher(c)
|
||||
if rw, ok := w.(recursiveWatcher); ok {
|
||||
return newRecursiveTree(rw, c)
|
||||
}
|
||||
return newNonrecursiveTree(w, c, make(chan EventInfo, buffer))
|
||||
}
|
||||
+292
@@ -0,0 +1,292 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package notify
|
||||
|
||||
import "sync"
|
||||
|
||||
// nonrecursiveTree TODO(rjeczalik)
|
||||
type nonrecursiveTree struct {
|
||||
rw sync.RWMutex // protects root
|
||||
root root
|
||||
w watcher
|
||||
c chan EventInfo
|
||||
rec chan EventInfo
|
||||
}
|
||||
|
||||
// newNonrecursiveTree TODO(rjeczalik)
|
||||
func newNonrecursiveTree(w watcher, c, rec chan EventInfo) *nonrecursiveTree {
|
||||
if rec == nil {
|
||||
rec = make(chan EventInfo, buffer)
|
||||
}
|
||||
t := &nonrecursiveTree{
|
||||
root: root{nd: newnode("")},
|
||||
w: w,
|
||||
c: c,
|
||||
rec: rec,
|
||||
}
|
||||
go t.dispatch(c)
|
||||
go t.internal(rec)
|
||||
return t
|
||||
}
|
||||
|
||||
// dispatch TODO(rjeczalik)
|
||||
func (t *nonrecursiveTree) dispatch(c <-chan EventInfo) {
|
||||
for ei := range c {
|
||||
dbgprintf("dispatching %v on %q", ei.Event(), ei.Path())
|
||||
go func(ei EventInfo) {
|
||||
var nd node
|
||||
var isrec bool
|
||||
dir, base := split(ei.Path())
|
||||
fn := func(it node, isbase bool) error {
|
||||
isrec = isrec || it.Watch.IsRecursive()
|
||||
if isbase {
|
||||
nd = it
|
||||
} else {
|
||||
it.Watch.Dispatch(ei, recursive)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
t.rw.RLock()
|
||||
// Notify recursive watchpoints found on the path.
|
||||
if err := t.root.WalkPath(dir, fn); err != nil {
|
||||
dbgprint("dispatch did not reach leaf:", err)
|
||||
t.rw.RUnlock()
|
||||
return
|
||||
}
|
||||
// Notify parent watchpoint.
|
||||
nd.Watch.Dispatch(ei, 0)
|
||||
isrec = isrec || nd.Watch.IsRecursive()
|
||||
// If leaf watchpoint exists, notify it.
|
||||
if nd, ok := nd.Child[base]; ok {
|
||||
isrec = isrec || nd.Watch.IsRecursive()
|
||||
nd.Watch.Dispatch(ei, 0)
|
||||
}
|
||||
t.rw.RUnlock()
|
||||
// If the event describes newly leaf directory created within
|
||||
if !isrec || ei.Event() != Create {
|
||||
return
|
||||
}
|
||||
if ok, err := ei.(isDirer).isDir(); !ok || err != nil {
|
||||
return
|
||||
}
|
||||
t.rec <- ei
|
||||
}(ei)
|
||||
}
|
||||
}
|
||||
|
||||
// internal TODO(rjeczalik)
|
||||
func (t *nonrecursiveTree) internal(rec <-chan EventInfo) {
|
||||
for ei := range rec {
|
||||
var nd node
|
||||
var eset = internal
|
||||
t.rw.Lock()
|
||||
t.root.WalkPath(ei.Path(), func(it node, _ bool) error {
|
||||
if e := it.Watch[t.rec]; e != 0 && e > eset {
|
||||
eset = e
|
||||
}
|
||||
nd = it
|
||||
return nil
|
||||
})
|
||||
if eset == internal {
|
||||
t.rw.Unlock()
|
||||
continue
|
||||
}
|
||||
err := nd.Add(ei.Path()).AddDir(t.recFunc(eset))
|
||||
t.rw.Unlock()
|
||||
if err != nil {
|
||||
dbgprintf("internal(%p) error: %v", rec, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// watchAdd TODO(rjeczalik)
|
||||
func (t *nonrecursiveTree) watchAdd(nd node, c chan<- EventInfo, e Event) eventDiff {
|
||||
if e&recursive != 0 {
|
||||
diff := nd.Watch.Add(t.rec, e|Create|omit)
|
||||
nd.Watch.Add(c, e)
|
||||
return diff
|
||||
}
|
||||
return nd.Watch.Add(c, e)
|
||||
}
|
||||
|
||||
// watchDelMin TODO(rjeczalik)
|
||||
func (t *nonrecursiveTree) watchDelMin(min Event, nd node, c chan<- EventInfo, e Event) eventDiff {
|
||||
old, ok := nd.Watch[t.rec]
|
||||
if ok {
|
||||
nd.Watch[t.rec] = min
|
||||
}
|
||||
diff := nd.Watch.Del(c, e)
|
||||
if ok {
|
||||
switch old &^= diff[0] &^ diff[1]; {
|
||||
case old|internal == internal:
|
||||
delete(nd.Watch, t.rec)
|
||||
if set, ok := nd.Watch[nil]; ok && len(nd.Watch) == 1 && set == 0 {
|
||||
delete(nd.Watch, nil)
|
||||
}
|
||||
default:
|
||||
nd.Watch.Add(t.rec, old|Create)
|
||||
switch {
|
||||
case diff == none:
|
||||
case diff[1]|Create == diff[0]:
|
||||
diff = none
|
||||
default:
|
||||
diff[1] |= Create
|
||||
}
|
||||
}
|
||||
}
|
||||
return diff
|
||||
}
|
||||
|
||||
// watchDel TODO(rjeczalik)
|
||||
func (t *nonrecursiveTree) watchDel(nd node, c chan<- EventInfo, e Event) eventDiff {
|
||||
return t.watchDelMin(0, nd, c, e)
|
||||
}
|
||||
|
||||
// Watch TODO(rjeczalik)
|
||||
func (t *nonrecursiveTree) Watch(path string, c chan<- EventInfo, events ...Event) error {
|
||||
if c == nil {
|
||||
panic("notify: Watch using nil channel")
|
||||
}
|
||||
// Expanding with empty event set is a nop.
|
||||
if len(events) == 0 {
|
||||
return nil
|
||||
}
|
||||
path, isrec, err := cleanpath(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
eset := joinevents(events)
|
||||
t.rw.Lock()
|
||||
defer t.rw.Unlock()
|
||||
nd := t.root.Add(path)
|
||||
if isrec {
|
||||
return t.watchrec(nd, c, eset|recursive)
|
||||
}
|
||||
return t.watch(nd, c, eset)
|
||||
}
|
||||
|
||||
func (t *nonrecursiveTree) watch(nd node, c chan<- EventInfo, e Event) (err error) {
|
||||
diff := nd.Watch.Add(c, e)
|
||||
switch {
|
||||
case diff == none:
|
||||
return nil
|
||||
case diff[1] == 0:
|
||||
// TODO(rjeczalik): cleanup this panic after implementation is stable
|
||||
panic("eset is empty: " + nd.Name)
|
||||
case diff[0] == 0:
|
||||
err = t.w.Watch(nd.Name, diff[1])
|
||||
default:
|
||||
err = t.w.Rewatch(nd.Name, diff[0], diff[1])
|
||||
}
|
||||
if err != nil {
|
||||
nd.Watch.Del(c, diff.Event())
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *nonrecursiveTree) recFunc(e Event) walkFunc {
|
||||
return func(nd node) error {
|
||||
switch diff := nd.Watch.Add(t.rec, e|omit|Create); {
|
||||
case diff == none:
|
||||
case diff[1] == 0:
|
||||
// TODO(rjeczalik): cleanup this panic after implementation is stable
|
||||
panic("eset is empty: " + nd.Name)
|
||||
case diff[0] == 0:
|
||||
t.w.Watch(nd.Name, diff[1])
|
||||
default:
|
||||
t.w.Rewatch(nd.Name, diff[0], diff[1])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (t *nonrecursiveTree) watchrec(nd node, c chan<- EventInfo, e Event) error {
|
||||
var traverse func(walkFunc) error
|
||||
// Non-recursive tree listens on Create event for every recursive
|
||||
// watchpoint in order to automagically set a watch for every
|
||||
// created directory.
|
||||
switch diff := nd.Watch.dryAdd(t.rec, e|Create); {
|
||||
case diff == none:
|
||||
t.watchAdd(nd, c, e)
|
||||
nd.Watch.Add(t.rec, e|omit|Create)
|
||||
return nil
|
||||
case diff[1] == 0:
|
||||
// TODO(rjeczalik): cleanup this panic after implementation is stable
|
||||
panic("eset is empty: " + nd.Name)
|
||||
case diff[0] == 0:
|
||||
// TODO(rjeczalik): BFS into directories and skip subtree as soon as first
|
||||
// recursive watchpoint is encountered.
|
||||
traverse = nd.AddDir
|
||||
default:
|
||||
traverse = nd.Walk
|
||||
}
|
||||
// TODO(rjeczalik): account every path that failed to be (re)watched
|
||||
// and retry.
|
||||
if err := traverse(t.recFunc(e)); err != nil {
|
||||
return err
|
||||
}
|
||||
t.watchAdd(nd, c, e)
|
||||
return nil
|
||||
}
|
||||
|
||||
type walkWatchpointFunc func(Event, node) error
|
||||
|
||||
func (t *nonrecursiveTree) walkWatchpoint(nd node, fn walkWatchpointFunc) error {
|
||||
type minode struct {
|
||||
min Event
|
||||
nd node
|
||||
}
|
||||
mnd := minode{nd: nd}
|
||||
stack := []minode{mnd}
|
||||
Traverse:
|
||||
for n := len(stack); n != 0; n = len(stack) {
|
||||
mnd, stack = stack[n-1], stack[:n-1]
|
||||
// There must be no recursive watchpoints if the node has no watchpoints
|
||||
// itself (every node in subtree rooted at recursive watchpoints must
|
||||
// have at least nil (total) and t.rec watchpoints).
|
||||
if len(mnd.nd.Watch) != 0 {
|
||||
switch err := fn(mnd.min, mnd.nd); err {
|
||||
case nil:
|
||||
case errSkip:
|
||||
continue Traverse
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, nd := range mnd.nd.Child {
|
||||
stack = append(stack, minode{mnd.nd.Watch[t.rec], nd})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop TODO(rjeczalik)
|
||||
func (t *nonrecursiveTree) Stop(c chan<- EventInfo) {
|
||||
fn := func(min Event, nd node) error {
|
||||
// TODO(rjeczalik): aggregate watcher errors and retry; in worst case
|
||||
// forward to the user.
|
||||
switch diff := t.watchDelMin(min, nd, c, all); {
|
||||
case diff == none:
|
||||
return nil
|
||||
case diff[1] == 0:
|
||||
t.w.Unwatch(nd.Name)
|
||||
default:
|
||||
t.w.Rewatch(nd.Name, diff[0], diff[1])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
t.rw.Lock()
|
||||
err := t.walkWatchpoint(t.root.nd, fn) // TODO(rjeczalik): store max root per c
|
||||
t.rw.Unlock()
|
||||
dbgprintf("Stop(%p) error: %v\n", c, err)
|
||||
}
|
||||
|
||||
// Close TODO(rjeczalik)
|
||||
func (t *nonrecursiveTree) Close() error {
|
||||
err := t.w.Close()
|
||||
close(t.c)
|
||||
return err
|
||||
}
|
||||
+543
@@ -0,0 +1,543 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNonrecursiveTree(t *testing.T) {
|
||||
n := NewNonrecursiveTreeTest(t, "testdata/vfs.txt")
|
||||
defer n.Close()
|
||||
|
||||
ch := NewChans(5)
|
||||
|
||||
watches := [...]RCase{
|
||||
// i=0
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/rjeczalik/fs/fs.go",
|
||||
C: ch[0],
|
||||
E: Rename,
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/rjeczalik/fs/fs.go",
|
||||
E: Rename,
|
||||
},
|
||||
},
|
||||
},
|
||||
// i=1
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/rjeczalik/fs/cmd/...",
|
||||
C: ch[1],
|
||||
E: Remove,
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/rjeczalik/fs/cmd",
|
||||
E: Create | Remove,
|
||||
},
|
||||
{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/rjeczalik/fs/cmd/gotree",
|
||||
E: Create | Remove,
|
||||
},
|
||||
{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/rjeczalik/fs/cmd/mktree",
|
||||
E: Create | Remove,
|
||||
},
|
||||
},
|
||||
},
|
||||
// i=2
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/rjeczalik/fs/cmd/...",
|
||||
C: ch[2],
|
||||
E: Rename,
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncRewatch,
|
||||
P: "src/github.com/rjeczalik/fs/cmd",
|
||||
E: Create | Remove,
|
||||
NE: Create | Remove | Rename,
|
||||
},
|
||||
{
|
||||
F: FuncRewatch,
|
||||
P: "src/github.com/rjeczalik/fs/cmd/gotree",
|
||||
E: Create | Remove,
|
||||
NE: Create | Remove | Rename,
|
||||
},
|
||||
{
|
||||
F: FuncRewatch,
|
||||
P: "src/github.com/rjeczalik/fs/cmd/mktree",
|
||||
E: Create | Remove,
|
||||
NE: Create | Remove | Rename,
|
||||
},
|
||||
},
|
||||
},
|
||||
// i=3
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/rjeczalik/fs/cmd/mktree/...",
|
||||
C: ch[2],
|
||||
E: Write,
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncRewatch,
|
||||
P: "src/github.com/rjeczalik/fs/cmd/mktree",
|
||||
E: Create | Remove | Rename,
|
||||
NE: Create | Remove | Rename | Write,
|
||||
},
|
||||
},
|
||||
},
|
||||
// i=4
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/pblaszczyk/qttu/include",
|
||||
C: ch[3],
|
||||
E: Create,
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/pblaszczyk/qttu/include",
|
||||
E: Create,
|
||||
},
|
||||
},
|
||||
},
|
||||
// i=5
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/pblaszczyk/qttu/include/qttu/detail/...",
|
||||
C: ch[3],
|
||||
E: Write,
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/pblaszczyk/qttu/include/qttu/detail",
|
||||
E: Create | Write,
|
||||
},
|
||||
},
|
||||
},
|
||||
// i=6
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/pblaszczyk/qttu/include/...",
|
||||
C: ch[0],
|
||||
E: Rename,
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncRewatch,
|
||||
P: "src/github.com/pblaszczyk/qttu/include",
|
||||
E: Create,
|
||||
NE: Create | Rename,
|
||||
},
|
||||
{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/pblaszczyk/qttu/include/qttu",
|
||||
E: Create | Rename,
|
||||
},
|
||||
{
|
||||
F: FuncRewatch,
|
||||
P: "src/github.com/pblaszczyk/qttu/include/qttu/detail",
|
||||
E: Create | Write,
|
||||
NE: Create | Write | Rename,
|
||||
},
|
||||
},
|
||||
},
|
||||
// i=7
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/pblaszczyk/...",
|
||||
C: ch[1],
|
||||
E: Write,
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/pblaszczyk",
|
||||
E: Create | Write,
|
||||
},
|
||||
{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/pblaszczyk/qttu",
|
||||
E: Create | Write,
|
||||
},
|
||||
{
|
||||
F: FuncRewatch,
|
||||
P: "src/github.com/pblaszczyk/qttu/include",
|
||||
E: Create | Rename,
|
||||
NE: Create | Rename | Write,
|
||||
},
|
||||
{
|
||||
F: FuncRewatch,
|
||||
P: "src/github.com/pblaszczyk/qttu/include/qttu",
|
||||
E: Create | Rename,
|
||||
NE: Create | Rename | Write,
|
||||
},
|
||||
{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/pblaszczyk/qttu/src",
|
||||
E: Create | Write,
|
||||
},
|
||||
},
|
||||
},
|
||||
// i=8
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/pblaszczyk/qttu/include/...",
|
||||
C: ch[4],
|
||||
E: Write,
|
||||
},
|
||||
Record: nil,
|
||||
},
|
||||
// i=9
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/pblaszczyk/qttu",
|
||||
C: ch[3],
|
||||
E: Remove,
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncRewatch,
|
||||
P: "src/github.com/pblaszczyk/qttu",
|
||||
E: Create | Write,
|
||||
NE: Create | Write | Remove,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
n.ExpectRecordedCalls(watches[:])
|
||||
|
||||
events := [...]TCase{
|
||||
// i=0
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs/fs.go", E: Rename},
|
||||
Receiver: Chans{ch[0]},
|
||||
},
|
||||
// i=1
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs/fs.go", E: Create},
|
||||
Receiver: nil,
|
||||
},
|
||||
// i=2
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs/cmd/cmd.go", E: Remove},
|
||||
Receiver: Chans{ch[1]},
|
||||
},
|
||||
// i=3
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs/cmd/doc.go", E: Write},
|
||||
Receiver: nil,
|
||||
},
|
||||
// i=4
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs/cmd/mktree/main.go", E: Write},
|
||||
Receiver: Chans{ch[2]},
|
||||
},
|
||||
// i=5
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs/cmd/mktree/tree.go", E: Create},
|
||||
Receiver: nil,
|
||||
},
|
||||
// i=6
|
||||
{
|
||||
Event: Call{P: "src/github.com/pblaszczyk/qttu/include/.lock", E: Create},
|
||||
Receiver: Chans{ch[3]},
|
||||
},
|
||||
// i=7
|
||||
{
|
||||
Event: Call{P: "src/github.com/pblaszczyk/qttu/include/qttu/detail/registry.hh", E: Write},
|
||||
Receiver: Chans{ch[3], ch[1], ch[4]},
|
||||
},
|
||||
// i=8
|
||||
{
|
||||
Event: Call{P: "src/github.com/pblaszczyk/qttu/include/qttu", E: Remove},
|
||||
Receiver: nil,
|
||||
},
|
||||
// i=9
|
||||
{
|
||||
Event: Call{P: "src/github.com/pblaszczyk/qttu/include", E: Remove},
|
||||
Receiver: Chans{ch[3]},
|
||||
},
|
||||
}
|
||||
|
||||
n.ExpectTreeEvents(events[:], ch)
|
||||
|
||||
stops := [...]RCase{
|
||||
// i=0
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncStop,
|
||||
C: ch[4],
|
||||
},
|
||||
Record: nil,
|
||||
},
|
||||
// i=1
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncStop,
|
||||
C: ch[3],
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncRewatch,
|
||||
P: "src/github.com/pblaszczyk/qttu",
|
||||
E: Create | Write | Remove,
|
||||
NE: Create | Write,
|
||||
},
|
||||
},
|
||||
},
|
||||
// i=2
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncStop,
|
||||
C: ch[2],
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncRewatch,
|
||||
P: "src/github.com/rjeczalik/fs/cmd",
|
||||
E: Create | Remove | Rename,
|
||||
NE: Create | Remove,
|
||||
},
|
||||
{
|
||||
F: FuncRewatch,
|
||||
P: "src/github.com/rjeczalik/fs/cmd/gotree",
|
||||
E: Create | Remove | Rename,
|
||||
NE: Create | Remove,
|
||||
},
|
||||
{
|
||||
F: FuncRewatch,
|
||||
P: "src/github.com/rjeczalik/fs/cmd/mktree",
|
||||
E: Create | Remove | Rename | Write,
|
||||
NE: Create | Remove,
|
||||
},
|
||||
},
|
||||
},
|
||||
// i=3
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncStop,
|
||||
C: ch[1],
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncUnwatch,
|
||||
P: "src/github.com/pblaszczyk",
|
||||
},
|
||||
{
|
||||
F: FuncUnwatch,
|
||||
P: "src/github.com/pblaszczyk/qttu",
|
||||
},
|
||||
{
|
||||
F: FuncRewatch,
|
||||
P: "src/github.com/pblaszczyk/qttu/include",
|
||||
E: Create | Rename | Write,
|
||||
NE: Create | Rename,
|
||||
},
|
||||
{
|
||||
F: FuncRewatch,
|
||||
P: "src/github.com/pblaszczyk/qttu/include/qttu",
|
||||
E: Create | Rename | Write,
|
||||
NE: Create | Rename,
|
||||
},
|
||||
{
|
||||
F: FuncRewatch,
|
||||
P: "src/github.com/pblaszczyk/qttu/include/qttu/detail",
|
||||
E: Create | Rename | Write,
|
||||
NE: Create | Rename,
|
||||
},
|
||||
{
|
||||
F: FuncUnwatch,
|
||||
P: "src/github.com/pblaszczyk/qttu/src",
|
||||
},
|
||||
{
|
||||
F: FuncUnwatch,
|
||||
P: "src/github.com/rjeczalik/fs/cmd",
|
||||
},
|
||||
{
|
||||
F: FuncUnwatch,
|
||||
P: "src/github.com/rjeczalik/fs/cmd/gotree",
|
||||
},
|
||||
{
|
||||
F: FuncUnwatch,
|
||||
P: "src/github.com/rjeczalik/fs/cmd/mktree",
|
||||
},
|
||||
},
|
||||
},
|
||||
// i=4
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncStop,
|
||||
C: ch[0],
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncUnwatch,
|
||||
P: "src/github.com/pblaszczyk/qttu/include",
|
||||
},
|
||||
{
|
||||
F: FuncUnwatch,
|
||||
P: "src/github.com/pblaszczyk/qttu/include/qttu",
|
||||
},
|
||||
{
|
||||
F: FuncUnwatch,
|
||||
P: "src/github.com/pblaszczyk/qttu/include/qttu/detail",
|
||||
},
|
||||
{
|
||||
F: FuncUnwatch,
|
||||
P: "src/github.com/rjeczalik/fs/fs.go",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
n.ExpectRecordedCalls(stops[:])
|
||||
|
||||
n.Walk(func(nd node) error {
|
||||
if len(nd.Watch) != 0 {
|
||||
return fmt.Errorf("unexpected watchpoint: name=%s, eventset=%v (len=%d)",
|
||||
nd.Name, nd.Watch.Total(), len(nd.Watch))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func TestNonrecursiveTreeInternal(t *testing.T) {
|
||||
n, c := NewNonrecursiveTreeTestC(t, "testdata/vfs.txt")
|
||||
defer n.Close()
|
||||
|
||||
ch := NewChans(5)
|
||||
|
||||
watches := [...]RCase{
|
||||
// i=0
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/rjeczalik/fs/cmd/...",
|
||||
C: ch[0],
|
||||
E: Remove,
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/rjeczalik/fs/cmd",
|
||||
E: Create | Remove,
|
||||
},
|
||||
{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/rjeczalik/fs/cmd/gotree",
|
||||
E: Create | Remove,
|
||||
},
|
||||
{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/rjeczalik/fs/cmd/mktree",
|
||||
E: Create | Remove,
|
||||
},
|
||||
},
|
||||
},
|
||||
// i=1
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/ppknap/link/include/coost/...",
|
||||
C: ch[1],
|
||||
E: Create,
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/ppknap/link/include/coost",
|
||||
E: Create,
|
||||
},
|
||||
{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/ppknap/link/include/coost/link",
|
||||
E: Create,
|
||||
},
|
||||
{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/ppknap/link/include/coost/link/detail",
|
||||
E: Create,
|
||||
},
|
||||
{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/ppknap/link/include/coost/link/detail/stdhelpers",
|
||||
E: Create,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
n.ExpectRecordedCalls(watches[:])
|
||||
|
||||
events := [...]TCase{
|
||||
// i=0
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs/cmd/dir", E: Create, Dir: true},
|
||||
Receiver: Chans{c},
|
||||
},
|
||||
// i=1
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs/cmd/dir/another", E: Create, Dir: true},
|
||||
Receiver: Chans{c},
|
||||
},
|
||||
// i=2
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs/cmd/file", E: Create, Dir: false},
|
||||
Receiver: nil,
|
||||
},
|
||||
// i=3
|
||||
{
|
||||
Event: Call{P: "src/github.com/ppknap/link/include/coost/dir", E: Create, Dir: true},
|
||||
Receiver: Chans{ch[1], c},
|
||||
},
|
||||
// i=4
|
||||
{
|
||||
Event: Call{P: "src/github.com/ppknap/link/include/coost/dir/another", E: Create, Dir: true},
|
||||
Receiver: Chans{ch[1], c},
|
||||
},
|
||||
// i=5
|
||||
{
|
||||
Event: Call{P: "src/github.com/ppknap/link/include/coost/file", E: Create, Dir: false},
|
||||
Receiver: Chans{ch[1]},
|
||||
},
|
||||
// i=6
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs/cmd/mktree", E: Remove},
|
||||
Receiver: Chans{ch[0]},
|
||||
},
|
||||
// i=7
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs/cmd/rmtree", E: Create, Dir: true},
|
||||
Receiver: Chans{c},
|
||||
},
|
||||
}
|
||||
|
||||
n.ExpectTreeEvents(events[:], ch)
|
||||
}
|
||||
+354
@@ -0,0 +1,354 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package notify
|
||||
|
||||
import "sync"
|
||||
|
||||
// watchAdd TODO(rjeczalik)
|
||||
func watchAdd(nd node, c chan<- EventInfo, e Event) eventDiff {
|
||||
diff := nd.Watch.Add(c, e)
|
||||
if wp := nd.Child[""].Watch; len(wp) != 0 {
|
||||
e = wp.Total()
|
||||
diff[0] |= e
|
||||
diff[1] |= e
|
||||
if diff[0] == diff[1] {
|
||||
return none
|
||||
}
|
||||
}
|
||||
return diff
|
||||
}
|
||||
|
||||
// watchAddInactive TODO(rjeczalik)
|
||||
func watchAddInactive(nd node, c chan<- EventInfo, e Event) eventDiff {
|
||||
wp := nd.Child[""].Watch
|
||||
if wp == nil {
|
||||
wp = make(watchpoint)
|
||||
nd.Child[""] = node{Watch: wp}
|
||||
}
|
||||
diff := wp.Add(c, e)
|
||||
e = nd.Watch.Total()
|
||||
diff[0] |= e
|
||||
diff[1] |= e
|
||||
if diff[0] == diff[1] {
|
||||
return none
|
||||
}
|
||||
return diff
|
||||
}
|
||||
|
||||
// watchCopy TODO(rjeczalik)
|
||||
func watchCopy(src, dst node) {
|
||||
for c, e := range src.Watch {
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
watchAddInactive(dst, c, e)
|
||||
}
|
||||
if wpsrc := src.Child[""].Watch; len(wpsrc) != 0 {
|
||||
wpdst := dst.Child[""].Watch
|
||||
for c, e := range wpsrc {
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
wpdst.Add(c, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// watchDel TODO(rjeczalik)
|
||||
func watchDel(nd node, c chan<- EventInfo, e Event) eventDiff {
|
||||
diff := nd.Watch.Del(c, e)
|
||||
if wp := nd.Child[""].Watch; len(wp) != 0 {
|
||||
diffInactive := wp.Del(c, e)
|
||||
e = wp.Total()
|
||||
// TODO(rjeczalik): add e if e != all?
|
||||
diff[0] |= diffInactive[0] | e
|
||||
diff[1] |= diffInactive[1] | e
|
||||
if diff[0] == diff[1] {
|
||||
return none
|
||||
}
|
||||
}
|
||||
return diff
|
||||
}
|
||||
|
||||
// watchTotal TODO(rjeczalik)
|
||||
func watchTotal(nd node) Event {
|
||||
e := nd.Watch.Total()
|
||||
if wp := nd.Child[""].Watch; len(wp) != 0 {
|
||||
e |= wp.Total()
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// watchIsRecursive TODO(rjeczalik)
|
||||
func watchIsRecursive(nd node) bool {
|
||||
ok := nd.Watch.IsRecursive()
|
||||
// TODO(rjeczalik): add a test for len(wp) != 0 change the condition.
|
||||
if wp := nd.Child[""].Watch; len(wp) != 0 {
|
||||
// If a watchpoint holds inactive watchpoints, it means it's a parent
|
||||
// one, which is recursive by nature even though it may be not recursive
|
||||
// itself.
|
||||
ok = true
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
// recursiveTree TODO(rjeczalik)
|
||||
type recursiveTree struct {
|
||||
rw sync.RWMutex // protects root
|
||||
root root
|
||||
// TODO(rjeczalik): merge watcher + recursiveWatcher after #5 and #6
|
||||
w interface {
|
||||
watcher
|
||||
recursiveWatcher
|
||||
}
|
||||
c chan EventInfo
|
||||
}
|
||||
|
||||
// newRecursiveTree TODO(rjeczalik)
|
||||
func newRecursiveTree(w recursiveWatcher, c chan EventInfo) *recursiveTree {
|
||||
t := &recursiveTree{
|
||||
root: root{nd: newnode("")},
|
||||
w: struct {
|
||||
watcher
|
||||
recursiveWatcher
|
||||
}{w.(watcher), w},
|
||||
c: c,
|
||||
}
|
||||
go t.dispatch()
|
||||
return t
|
||||
}
|
||||
|
||||
// dispatch TODO(rjeczalik)
|
||||
func (t *recursiveTree) dispatch() {
|
||||
for ei := range t.c {
|
||||
dbgprintf("dispatching %v on %q", ei.Event(), ei.Path())
|
||||
go func(ei EventInfo) {
|
||||
nd, ok := node{}, false
|
||||
dir, base := split(ei.Path())
|
||||
fn := func(it node, isbase bool) error {
|
||||
if isbase {
|
||||
nd = it
|
||||
} else {
|
||||
it.Watch.Dispatch(ei, recursive)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
t.rw.RLock()
|
||||
defer t.rw.RUnlock()
|
||||
// Notify recursive watchpoints found on the path.
|
||||
if err := t.root.WalkPath(dir, fn); err != nil {
|
||||
dbgprint("dispatch did not reach leaf:", err)
|
||||
return
|
||||
}
|
||||
// Notify parent watchpoint.
|
||||
nd.Watch.Dispatch(ei, 0)
|
||||
// If leaf watchpoint exists, notify it.
|
||||
if nd, ok = nd.Child[base]; ok {
|
||||
nd.Watch.Dispatch(ei, 0)
|
||||
}
|
||||
}(ei)
|
||||
}
|
||||
}
|
||||
|
||||
// Watch TODO(rjeczalik)
|
||||
func (t *recursiveTree) Watch(path string, c chan<- EventInfo, events ...Event) error {
|
||||
if c == nil {
|
||||
panic("notify: Watch using nil channel")
|
||||
}
|
||||
// Expanding with empty event set is a nop.
|
||||
if len(events) == 0 {
|
||||
return nil
|
||||
}
|
||||
path, isrec, err := cleanpath(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
eventset := joinevents(events)
|
||||
if isrec {
|
||||
eventset |= recursive
|
||||
}
|
||||
t.rw.Lock()
|
||||
defer t.rw.Unlock()
|
||||
// case 1: cur is a child
|
||||
//
|
||||
// Look for parent watch which already covers the given path.
|
||||
parent := node{}
|
||||
self := false
|
||||
err = t.root.WalkPath(path, func(nd node, isbase bool) error {
|
||||
if watchTotal(nd) != 0 {
|
||||
parent = nd
|
||||
self = isbase
|
||||
return errSkip
|
||||
}
|
||||
return nil
|
||||
})
|
||||
cur := t.root.Add(path) // add after the walk, so it's less to traverse
|
||||
if err == nil && parent.Watch != nil {
|
||||
// Parent watch found. Register inactive watchpoint, so we have enough
|
||||
// information to shrink the eventset on eventual Stop.
|
||||
// return t.resetwatchpoint(parent, parent, c, eventset|inactive)
|
||||
var diff eventDiff
|
||||
if self {
|
||||
diff = watchAdd(cur, c, eventset)
|
||||
} else {
|
||||
diff = watchAddInactive(parent, c, eventset)
|
||||
}
|
||||
switch {
|
||||
case diff == none:
|
||||
// the parent watchpoint already covers requested subtree with its
|
||||
// eventset
|
||||
case diff[0] == 0:
|
||||
// TODO(rjeczalik): cleanup this panic after implementation is stable
|
||||
panic("dangling watchpoint: " + parent.Name)
|
||||
default:
|
||||
if isrec || watchIsRecursive(parent) {
|
||||
err = t.w.RecursiveRewatch(parent.Name, parent.Name, diff[0], diff[1])
|
||||
} else {
|
||||
err = t.w.Rewatch(parent.Name, diff[0], diff[1])
|
||||
}
|
||||
if err != nil {
|
||||
watchDel(parent, c, diff.Event())
|
||||
return err
|
||||
}
|
||||
watchAdd(cur, c, eventset)
|
||||
// TODO(rjeczalik): account top-most path for c
|
||||
return nil
|
||||
}
|
||||
if !self {
|
||||
watchAdd(cur, c, eventset)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// case 2: cur is new parent
|
||||
//
|
||||
// Look for children nodes, unwatch n-1 of them and rewatch the last one.
|
||||
var children []node
|
||||
fn := func(nd node) error {
|
||||
if len(nd.Watch) == 0 {
|
||||
return nil
|
||||
}
|
||||
children = append(children, nd)
|
||||
return errSkip
|
||||
}
|
||||
switch must(cur.Walk(fn)); len(children) {
|
||||
case 0:
|
||||
// no child watches, cur holds a new watch
|
||||
case 1:
|
||||
watchAdd(cur, c, eventset) // TODO(rjeczalik): update cache c subtree root?
|
||||
watchCopy(children[0], cur)
|
||||
err = t.w.RecursiveRewatch(children[0].Name, cur.Name, watchTotal(children[0]),
|
||||
watchTotal(cur))
|
||||
if err != nil {
|
||||
// Clean inactive watchpoint. The c chan did not exist before.
|
||||
cur.Child[""] = node{}
|
||||
delete(cur.Watch, c)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
watchAdd(cur, c, eventset)
|
||||
// Copy children inactive watchpoints to the new parent.
|
||||
for _, nd := range children {
|
||||
watchCopy(nd, cur)
|
||||
}
|
||||
// Watch parent subtree.
|
||||
if err = t.w.RecursiveWatch(cur.Name, watchTotal(cur)); err != nil {
|
||||
// Clean inactive watchpoint. The c chan did not exist before.
|
||||
cur.Child[""] = node{}
|
||||
delete(cur.Watch, c)
|
||||
return err
|
||||
}
|
||||
// Unwatch children subtrees.
|
||||
var e error
|
||||
for _, nd := range children {
|
||||
if watchIsRecursive(nd) {
|
||||
e = t.w.RecursiveUnwatch(nd.Name)
|
||||
} else {
|
||||
e = t.w.Unwatch(nd.Name)
|
||||
}
|
||||
if e != nil {
|
||||
err = nonil(err, e)
|
||||
// TODO(rjeczalik): child is still watched, warn all its watchpoints
|
||||
// about possible duplicate events via Error event
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
// case 3: cur is new, alone node
|
||||
switch diff := watchAdd(cur, c, eventset); {
|
||||
case diff == none:
|
||||
// TODO(rjeczalik): cleanup this panic after implementation is stable
|
||||
panic("watch requested but no parent watchpoint found: " + cur.Name)
|
||||
case diff[0] == 0:
|
||||
if isrec {
|
||||
err = t.w.RecursiveWatch(cur.Name, diff[1])
|
||||
} else {
|
||||
err = t.w.Watch(cur.Name, diff[1])
|
||||
}
|
||||
if err != nil {
|
||||
watchDel(cur, c, diff.Event())
|
||||
return err
|
||||
}
|
||||
default:
|
||||
// TODO(rjeczalik): cleanup this panic after implementation is stable
|
||||
panic("watch requested but no parent watchpoint found: " + cur.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop TODO(rjeczalik)
|
||||
//
|
||||
// TODO(rjeczalik): Split parent watchpoint - transfer watches to children
|
||||
// if parent is no longer needed. This carries a risk that underlying
|
||||
// watcher calls could fail - reconsider if it's worth the effort.
|
||||
func (t *recursiveTree) Stop(c chan<- EventInfo) {
|
||||
var err error
|
||||
fn := func(nd node) (e error) {
|
||||
diff := watchDel(nd, c, all)
|
||||
switch {
|
||||
case diff == none && watchTotal(nd) == 0:
|
||||
// TODO(rjeczalik): There's no watchpoints deeper in the tree,
|
||||
// probably we should remove the nodes as well.
|
||||
return nil
|
||||
case diff == none:
|
||||
// Removing c from nd does not require shrinking its eventset.
|
||||
case diff[1] == 0:
|
||||
if watchIsRecursive(nd) {
|
||||
e = t.w.RecursiveUnwatch(nd.Name)
|
||||
} else {
|
||||
e = t.w.Unwatch(nd.Name)
|
||||
}
|
||||
default:
|
||||
if watchIsRecursive(nd) {
|
||||
e = t.w.RecursiveRewatch(nd.Name, nd.Name, diff[0], diff[1])
|
||||
} else {
|
||||
e = t.w.Rewatch(nd.Name, diff[0], diff[1])
|
||||
}
|
||||
}
|
||||
fn := func(nd node) error {
|
||||
watchDel(nd, c, all)
|
||||
return nil
|
||||
}
|
||||
err = nonil(err, e, nd.Walk(fn))
|
||||
// TODO(rjeczalik): if e != nil store dummy chan in nd.Watch just to
|
||||
// retry un/rewatching next time and/or let the user handle the failure
|
||||
// vie Error event?
|
||||
return errSkip
|
||||
}
|
||||
t.rw.Lock()
|
||||
e := t.root.Walk("", fn) // TODO(rjeczalik): use max root per c
|
||||
t.rw.Unlock()
|
||||
if e != nil {
|
||||
err = nonil(err, e)
|
||||
}
|
||||
dbgprintf("Stop(%p) error: %v\n", c, err)
|
||||
}
|
||||
|
||||
// Close TODO(rjeczalik)
|
||||
func (t *recursiveTree) Close() error {
|
||||
err := t.w.Close()
|
||||
close(t.c)
|
||||
return err
|
||||
}
|
||||
+524
@@ -0,0 +1,524 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package notify
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestRecursiveTree(t *testing.T) {
|
||||
n := NewRecursiveTreeTest(t, "testdata/vfs.txt")
|
||||
defer n.Close()
|
||||
|
||||
ch := NewChans(5)
|
||||
|
||||
watches := [...]RCase{
|
||||
// i=0
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/rjeczalik/fs/fs.go",
|
||||
C: ch[0],
|
||||
E: Create,
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/rjeczalik/fs/fs.go",
|
||||
E: Create,
|
||||
},
|
||||
},
|
||||
},
|
||||
// i=1
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/rjeczalik/fs/cmd/...",
|
||||
C: ch[1],
|
||||
E: Remove,
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncRecursiveWatch,
|
||||
P: "src/github.com/rjeczalik/fs/cmd",
|
||||
E: Remove,
|
||||
},
|
||||
},
|
||||
},
|
||||
// i=2
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/rjeczalik/fs",
|
||||
C: ch[2],
|
||||
E: Rename,
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncRecursiveWatch,
|
||||
P: "src/github.com/rjeczalik/fs",
|
||||
E: Create | Remove | Rename,
|
||||
},
|
||||
{
|
||||
F: FuncRecursiveUnwatch,
|
||||
P: "src/github.com/rjeczalik/fs/cmd",
|
||||
},
|
||||
{
|
||||
F: FuncUnwatch,
|
||||
P: "src/github.com/rjeczalik/fs/fs.go",
|
||||
},
|
||||
},
|
||||
},
|
||||
// i=3
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/ppknap/link/README.md",
|
||||
C: ch[0],
|
||||
E: Create,
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/ppknap/link/README.md",
|
||||
E: Create,
|
||||
},
|
||||
},
|
||||
},
|
||||
// i=4
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/ppknap/link/include/...",
|
||||
C: ch[3],
|
||||
E: Remove,
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncRecursiveWatch,
|
||||
P: "src/github.com/ppknap/link/include",
|
||||
E: Remove,
|
||||
},
|
||||
},
|
||||
},
|
||||
// i=5
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/ppknap/link/include",
|
||||
C: ch[0],
|
||||
E: Write,
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncRecursiveRewatch,
|
||||
P: "src/github.com/ppknap/link/include",
|
||||
NP: "src/github.com/ppknap/link/include",
|
||||
E: Remove,
|
||||
NE: Remove | Write,
|
||||
},
|
||||
},
|
||||
},
|
||||
// i=6
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/ppknap/link/test/Jamfile.jam",
|
||||
C: ch[0],
|
||||
E: Rename,
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/ppknap/link/test/Jamfile.jam",
|
||||
E: Rename,
|
||||
},
|
||||
},
|
||||
},
|
||||
// i=7
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/ppknap/link/test/Jamfile.jam",
|
||||
C: ch[0],
|
||||
E: Create,
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncRewatch,
|
||||
P: "src/github.com/ppknap/link/test/Jamfile.jam",
|
||||
E: Rename,
|
||||
NE: Rename | Create,
|
||||
},
|
||||
},
|
||||
},
|
||||
// i=8
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/ppknap/...",
|
||||
C: ch[0],
|
||||
E: Create,
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncRecursiveWatch,
|
||||
P: "src/github.com/ppknap",
|
||||
E: Create | Remove | Write | Rename,
|
||||
},
|
||||
{
|
||||
F: FuncUnwatch,
|
||||
P: "src/github.com/ppknap/link/README.md",
|
||||
},
|
||||
{
|
||||
F: FuncRecursiveUnwatch,
|
||||
P: "src/github.com/ppknap/link/include",
|
||||
},
|
||||
{
|
||||
F: FuncUnwatch,
|
||||
P: "src/github.com/ppknap/link/test/Jamfile.jam",
|
||||
},
|
||||
},
|
||||
},
|
||||
// i=9
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/rjeczalik/fs/README.md",
|
||||
C: ch[0],
|
||||
E: Rename,
|
||||
},
|
||||
Record: nil,
|
||||
},
|
||||
// i=10
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/rjeczalik/fs/cmd/gotree",
|
||||
C: ch[2],
|
||||
E: Create | Remove,
|
||||
},
|
||||
Record: nil,
|
||||
},
|
||||
// i=11
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/pblaszczyk/qttu/src/main.cc",
|
||||
C: ch[0],
|
||||
E: Create,
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/pblaszczyk/qttu/src/main.cc",
|
||||
E: Create,
|
||||
},
|
||||
},
|
||||
},
|
||||
// i=12
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/pblaszczyk/qttu/src/main.cc",
|
||||
C: ch[0],
|
||||
E: Remove,
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncRewatch,
|
||||
P: "src/github.com/pblaszczyk/qttu/src/main.cc",
|
||||
E: Create,
|
||||
NE: Create | Remove,
|
||||
},
|
||||
},
|
||||
},
|
||||
// i=13
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/pblaszczyk/qttu/src/main.cc",
|
||||
C: ch[0],
|
||||
E: Create | Remove,
|
||||
},
|
||||
Record: nil,
|
||||
},
|
||||
// i=14
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/pblaszczyk/qttu/src",
|
||||
C: ch[0],
|
||||
E: Create,
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncRecursiveRewatch,
|
||||
P: "src/github.com/pblaszczyk/qttu/src/main.cc",
|
||||
NP: "src/github.com/pblaszczyk/qttu/src",
|
||||
E: Create | Remove,
|
||||
NE: Create | Remove,
|
||||
},
|
||||
},
|
||||
},
|
||||
// i=15
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/pblaszczyk/qttu",
|
||||
C: ch[4],
|
||||
E: Write,
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncRecursiveRewatch,
|
||||
P: "src/github.com/pblaszczyk/qttu/src",
|
||||
NP: "src/github.com/pblaszczyk/qttu",
|
||||
E: Create | Remove,
|
||||
NE: Create | Remove | Write,
|
||||
},
|
||||
},
|
||||
},
|
||||
// i=16
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/rjeczalik/fs/fs.go",
|
||||
C: ch[3],
|
||||
E: Rename,
|
||||
},
|
||||
Record: nil,
|
||||
},
|
||||
}
|
||||
|
||||
n.ExpectRecordedCalls(watches[:])
|
||||
|
||||
events := [...]TCase{
|
||||
// i=0
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs/fs.go", E: Rename},
|
||||
Receiver: Chans{ch[2], ch[3]},
|
||||
},
|
||||
// i=1
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs/fs.go", E: Create},
|
||||
Receiver: Chans{ch[0]},
|
||||
},
|
||||
// i=2
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs/fs.go/file", E: Create},
|
||||
Receiver: Chans{ch[0]},
|
||||
},
|
||||
// i=3
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs", E: Rename},
|
||||
Receiver: Chans{ch[2]},
|
||||
},
|
||||
// i=4
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs/fs_test.go", E: Rename},
|
||||
Receiver: Chans{ch[2]},
|
||||
},
|
||||
// i=5
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs/cmd/mktree/main.go", E: Remove},
|
||||
Receiver: Chans{ch[1]},
|
||||
},
|
||||
// i=6
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs/cmd/gotree", E: Remove},
|
||||
Receiver: Chans{ch[1], ch[2]},
|
||||
},
|
||||
// i=7
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs/cmd", E: Remove},
|
||||
Receiver: Chans{ch[1]},
|
||||
},
|
||||
// i=8
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs/fs.go/file", E: Write},
|
||||
Receiver: nil,
|
||||
},
|
||||
// i=9
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs/fs.go/file", E: Write},
|
||||
Receiver: nil,
|
||||
},
|
||||
// i=10
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs", E: Remove},
|
||||
Receiver: nil,
|
||||
},
|
||||
// i=11
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs/cmd", E: Rename},
|
||||
Receiver: Chans{ch[2]},
|
||||
},
|
||||
// i=12
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs/cmd/mktree/main.go", E: Write},
|
||||
Receiver: nil,
|
||||
},
|
||||
// i=13
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs/cmd/gotree", E: Rename},
|
||||
Receiver: nil,
|
||||
},
|
||||
// i=14
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs/cmd/file", E: Rename},
|
||||
Receiver: nil,
|
||||
},
|
||||
// i=15
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs/fs.go", E: Rename},
|
||||
Receiver: Chans{ch[2], ch[3]},
|
||||
},
|
||||
}
|
||||
|
||||
n.ExpectTreeEvents(events[:], ch)
|
||||
|
||||
stops := [...]RCase{
|
||||
// i=0
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncStop,
|
||||
C: ch[1],
|
||||
},
|
||||
Record: nil,
|
||||
},
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncStop,
|
||||
C: ch[4],
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncRecursiveRewatch,
|
||||
P: "src/github.com/pblaszczyk/qttu",
|
||||
NP: "src/github.com/pblaszczyk/qttu",
|
||||
E: Create | Remove | Write,
|
||||
NE: Create | Remove,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
n.ExpectRecordedCalls(stops[:])
|
||||
}
|
||||
|
||||
func TestRecursiveTreeWatchInactiveMerge(t *testing.T) {
|
||||
n := NewRecursiveTreeTest(t, "testdata/vfs.txt")
|
||||
defer n.Close()
|
||||
|
||||
ch := NewChans(1)
|
||||
|
||||
watches := [...]RCase{
|
||||
// i=0
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/rjeczalik/fs",
|
||||
C: ch[0],
|
||||
E: Create,
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/rjeczalik/fs",
|
||||
E: Create,
|
||||
},
|
||||
},
|
||||
},
|
||||
// i=1
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/rjeczalik/fs/cmd/gotree/...",
|
||||
C: ch[0],
|
||||
E: Remove,
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncRecursiveRewatch,
|
||||
P: "src/github.com/rjeczalik/fs",
|
||||
NP: "src/github.com/rjeczalik/fs",
|
||||
E: Create,
|
||||
NE: Create | Remove,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
n.ExpectRecordedCalls(watches[:])
|
||||
|
||||
events := [...]TCase{
|
||||
// i=0
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs/.fs.go.swp", E: Create},
|
||||
Receiver: Chans{ch[0]},
|
||||
},
|
||||
// i=1
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs/.fs.go.swp", E: Remove},
|
||||
Receiver: nil,
|
||||
},
|
||||
// i=2
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs", E: Remove},
|
||||
Receiver: nil,
|
||||
},
|
||||
// i=3
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs/cmd/gotree/main.go", E: Remove},
|
||||
Receiver: Chans{ch[0]},
|
||||
},
|
||||
}
|
||||
|
||||
n.ExpectTreeEvents(events[:], ch)
|
||||
}
|
||||
|
||||
func TestRecursiveTree_Windows(t *testing.T) {
|
||||
n := NewRecursiveTreeTest(t, "testdata/vfs.txt")
|
||||
defer n.Close()
|
||||
|
||||
const ChangeFileName = Event(0x1)
|
||||
|
||||
ch := NewChans(1)
|
||||
|
||||
watches := [...]RCase{
|
||||
// i=0
|
||||
{
|
||||
Call: Call{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/rjeczalik/fs",
|
||||
C: ch[0],
|
||||
E: ChangeFileName,
|
||||
},
|
||||
Record: []Call{
|
||||
{
|
||||
F: FuncWatch,
|
||||
P: "src/github.com/rjeczalik/fs",
|
||||
E: ChangeFileName,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
n.ExpectRecordedCalls(watches[:])
|
||||
|
||||
events := [...]TCase{
|
||||
// i=0
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs", E: ChangeFileName},
|
||||
Receiver: Chans{ch[0]},
|
||||
},
|
||||
// i=1
|
||||
{
|
||||
Event: Call{P: "src/github.com/rjeczalik/fs/fs.go", E: ChangeFileName},
|
||||
Receiver: Chans{ch[0]},
|
||||
},
|
||||
}
|
||||
|
||||
n.ExpectTreeEvents(events[:], ch)
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const all = ^Event(0)
|
||||
const sep = string(os.PathSeparator)
|
||||
|
||||
var errDepth = errors.New("exceeded allowed iteration count (circular symlink?)")
|
||||
|
||||
func min(i, j int) int {
|
||||
if i > j {
|
||||
return j
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
func max(i, j int) int {
|
||||
if i < j {
|
||||
return j
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// must panics if err is non-nil.
|
||||
func must(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// nonil gives first non-nil error from the given arguments.
|
||||
func nonil(err ...error) error {
|
||||
for _, err := range err {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanpath(path string) (realpath string, isrec bool, err error) {
|
||||
if strings.HasSuffix(path, "...") {
|
||||
isrec = true
|
||||
path = path[:len(path)-3]
|
||||
}
|
||||
if path, err = filepath.Abs(path); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
if path, err = canonical(path); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
return path, isrec, nil
|
||||
}
|
||||
|
||||
// canonical resolves any symlink in the given path and returns it in a clean form.
|
||||
// It expects the path to be absolute. It fails to resolve circular symlinks by
|
||||
// maintaining a simple iteration limit.
|
||||
func canonical(p string) (string, error) {
|
||||
p, err := filepath.Abs(p)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for i, j, depth := 1, 0, 1; i < len(p); i, depth = i+1, depth+1 {
|
||||
if depth > 128 {
|
||||
return "", &os.PathError{Op: "canonical", Path: p, Err: errDepth}
|
||||
}
|
||||
if j = strings.IndexRune(p[i:], '/'); j == -1 {
|
||||
j, i = i, len(p)
|
||||
} else {
|
||||
j, i = i, i+j
|
||||
}
|
||||
fi, err := os.Lstat(p[:i])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||
s, err := os.Readlink(p[:i])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if filepath.IsAbs(s) {
|
||||
p = "/" + s + p[i:]
|
||||
} else {
|
||||
p = p[:j] + s + p[i:]
|
||||
}
|
||||
i = 1 // no guarantee s is canonical, start all over
|
||||
}
|
||||
}
|
||||
return filepath.Clean(p), nil
|
||||
}
|
||||
|
||||
func joinevents(events []Event) (e Event) {
|
||||
if len(events) == 0 {
|
||||
e = All
|
||||
} else {
|
||||
for _, event := range events {
|
||||
e |= event
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func split(s string) (string, string) {
|
||||
if i := lastIndexSep(s); i != -1 {
|
||||
return s[:i], s[i+1:]
|
||||
}
|
||||
return "", s
|
||||
}
|
||||
|
||||
func base(s string) string {
|
||||
if i := lastIndexSep(s); i != -1 {
|
||||
return s[i+1:]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func indexbase(root, name string) int {
|
||||
if n, m := len(root), len(name); m >= n && name[:n] == root &&
|
||||
(n == m || name[n] == os.PathSeparator) {
|
||||
return min(n+1, m)
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func indexSep(s string) int {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == os.PathSeparator {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func lastIndexSep(s string) int {
|
||||
for i := len(s) - 1; i >= 0; i-- {
|
||||
if s[i] == os.PathSeparator {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build darwin
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCanonicalDarwin(t *testing.T) {
|
||||
cases := [...]caseCanonical{
|
||||
{"/etc", "/private/etc"},
|
||||
{"/etc/defaults", "/private/etc/defaults"},
|
||||
{"/etc/hosts", "/private/etc/hosts"},
|
||||
{"/tmp", "/private/tmp"},
|
||||
{"/var", "/private/var"},
|
||||
}
|
||||
testCanonical(t, cases[:])
|
||||
}
|
||||
|
||||
func TestCanonicalDarwinMultiple(t *testing.T) {
|
||||
etcsym, err := symlink("/etc", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tmpsym, err := symlink("/tmp", "")
|
||||
if err != nil {
|
||||
t.Fatal(nonil(err, os.Remove(etcsym)))
|
||||
}
|
||||
defer removeall(etcsym, tmpsym)
|
||||
cases := [...]caseCanonical{
|
||||
{etcsym, "/private/etc"},
|
||||
{etcsym + "/hosts", "/private/etc/hosts"},
|
||||
{tmpsym, "/private/tmp"},
|
||||
}
|
||||
testCanonical(t, cases[:])
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type caseCanonical struct {
|
||||
path string
|
||||
full string
|
||||
}
|
||||
|
||||
func testCanonical(t *testing.T, cases []caseCanonical) {
|
||||
for i, cas := range cases {
|
||||
full, err := canonical(cas.path)
|
||||
if err != nil {
|
||||
t.Errorf("want err=nil; got %v (i=%d)", err, i)
|
||||
continue
|
||||
}
|
||||
if full != cas.full {
|
||||
t.Errorf("want full=%q; got %q (i=%d)", cas.full, full, i)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanonicalNoSymlink(t *testing.T) {
|
||||
td := filepath.Join(wd, "testdata")
|
||||
cases := [...]caseCanonical{
|
||||
{".", wd},
|
||||
{"testdata", td},
|
||||
{filepath.Join("testdata", ".."), wd},
|
||||
}
|
||||
testCanonical(t, cases[:])
|
||||
}
|
||||
|
||||
func TestJoinevents(t *testing.T) {
|
||||
cases := [...]struct {
|
||||
evs []Event
|
||||
ev Event
|
||||
}{
|
||||
0: {nil, All},
|
||||
1: {[]Event{}, All},
|
||||
2: {[]Event{Create}, Create},
|
||||
3: {[]Event{Rename}, Rename},
|
||||
4: {[]Event{Create, Write, Remove}, Create | Write | Remove},
|
||||
}
|
||||
for i, cas := range cases {
|
||||
if ev := joinevents(cas.evs); ev != cas.ev {
|
||||
t.Errorf("want event=%v; got %v (i=%d)", cas.ev, ev, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeSplit(t *testing.T) {
|
||||
cases := [...]struct {
|
||||
path string
|
||||
dir string
|
||||
base string
|
||||
}{
|
||||
{"/github.com/rjeczalik/fakerpc", "/github.com/rjeczalik", "fakerpc"},
|
||||
{"/home/rjeczalik/src", "/home/rjeczalik", "src"},
|
||||
{"/Users/pknap/porn/gopher.avi", "/Users/pknap/porn", "gopher.avi"},
|
||||
{"C:/Documents and Users", "C:", "Documents and Users"},
|
||||
{"C:/Documents and Users/pblaszczyk/wiertarka.exe", "C:/Documents and Users/pblaszczyk", "wiertarka.exe"},
|
||||
{"/home/(╯°□°)╯︵ ┻━┻", "/home", "(╯°□°)╯︵ ┻━┻"},
|
||||
}
|
||||
for i, cas := range cases {
|
||||
dir, base := split(filepath.FromSlash(cas.path))
|
||||
if want := filepath.FromSlash(cas.dir); dir != want {
|
||||
t.Errorf("want dir=%s; got %s (i=%d)", want, dir, i)
|
||||
}
|
||||
if want := filepath.FromSlash(cas.base); base != want {
|
||||
t.Errorf("want base=%s; got %s (i=%d)", want, base, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeBase(t *testing.T) {
|
||||
cases := [...]struct {
|
||||
path string
|
||||
base string
|
||||
}{
|
||||
{"/github.com/rjeczalik/fakerpc", "fakerpc"},
|
||||
{"/home/rjeczalik/src", "src"},
|
||||
{"/Users/pknap/porn/gopher.avi", "gopher.avi"},
|
||||
{"C:/Documents and Users", "Documents and Users"},
|
||||
{"C:/Documents and Users/pblaszczyk/wiertarka.exe", "wiertarka.exe"},
|
||||
{"/home/(╯°□°)╯︵ ┻━┻", "(╯°□°)╯︵ ┻━┻"},
|
||||
}
|
||||
for i, cas := range cases {
|
||||
if base := base(filepath.FromSlash(cas.path)); base != cas.base {
|
||||
t.Errorf("want base=%s; got %s (i=%d)", cas.base, base, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeIndexSep(t *testing.T) {
|
||||
cases := [...]struct {
|
||||
path string
|
||||
n int
|
||||
}{
|
||||
{"github.com/rjeczalik/fakerpc", 10},
|
||||
{"home/rjeczalik/src", 4},
|
||||
{"Users/pknap/porn/gopher.avi", 5},
|
||||
{"C:/Documents and Users", 2},
|
||||
{"Documents and Users/pblaszczyk/wiertarka.exe", 19},
|
||||
{"(╯°□°)╯︵ ┻━┻/Downloads", 30},
|
||||
}
|
||||
for i, cas := range cases {
|
||||
if n := indexSep(filepath.FromSlash(cas.path)); n != cas.n {
|
||||
t.Errorf("want n=%d; got %d (i=%d)", cas.n, n, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeLastIndexSep(t *testing.T) {
|
||||
cases := [...]struct {
|
||||
path string
|
||||
n int
|
||||
}{
|
||||
{"github.com/rjeczalik/fakerpc", 20},
|
||||
{"home/rjeczalik/src", 14},
|
||||
{"Users/pknap/porn/gopher.avi", 16},
|
||||
{"C:/Documents and Users", 2},
|
||||
{"Documents and Users/pblaszczyk/wiertarka.exe", 30},
|
||||
{"/home/(╯°□°)╯︵ ┻━┻", 5},
|
||||
}
|
||||
for i, cas := range cases {
|
||||
if n := lastIndexSep(filepath.FromSlash(cas.path)); n != cas.n {
|
||||
t.Errorf("want n=%d; got %d (i=%d)", cas.n, n, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanpath(t *testing.T) {
|
||||
t.Skip("TODO(rjeczalik)")
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build !windows
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func tmpfile(s string) (string, error) {
|
||||
f, err := ioutil.TempFile(filepath.Split(s))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err = nonil(f.Sync(), f.Close()); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return f.Name(), nil
|
||||
}
|
||||
|
||||
func symlink(src, dst string) (string, error) {
|
||||
name, err := tmpfile(dst)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err = nonil(os.Remove(name), os.Symlink(src, name)); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
||||
func removeall(s ...string) {
|
||||
for _, s := range s {
|
||||
os.Remove(s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanonical(t *testing.T) {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatalf("os.Getwd()=%v", err)
|
||||
}
|
||||
wdsym, err := symlink(wd, "")
|
||||
if err != nil {
|
||||
t.Fatalf(`symlink(%q, "")=%v`, wd, err)
|
||||
}
|
||||
td := filepath.Join(wd, "testdata")
|
||||
tdsym, err := symlink(td, td)
|
||||
if err != nil {
|
||||
t.Errorf("symlink(%q, %q)=%v", td, td, nonil(err, os.Remove(wdsym)))
|
||||
}
|
||||
defer removeall(wdsym, tdsym)
|
||||
vfstxt := filepath.Join(td, "vfs.txt")
|
||||
cases := [...]caseCanonical{
|
||||
{wdsym, wd},
|
||||
{tdsym, td},
|
||||
{filepath.Join(wdsym, "notify.go"), filepath.Join(wd, "notify.go")},
|
||||
{filepath.Join(tdsym, "vfs.txt"), vfstxt},
|
||||
{filepath.Join(wdsym, filepath.Base(tdsym), "vfs.txt"), vfstxt},
|
||||
}
|
||||
testCanonical(t, cases[:])
|
||||
}
|
||||
|
||||
func TestCanonicalCircular(t *testing.T) {
|
||||
tmp1, err := tmpfile("circular")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tmp2, err := tmpfile("circular")
|
||||
if err != nil {
|
||||
t.Fatal(nonil(err, os.Remove(tmp1)))
|
||||
}
|
||||
defer removeall(tmp1, tmp2)
|
||||
// Symlink tmp1 -> tmp2.
|
||||
if err = nonil(os.Remove(tmp1), os.Symlink(tmp2, tmp1)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Symlnik tmp2 -> tmp1.
|
||||
if err = nonil(os.Remove(tmp2), os.Symlink(tmp1, tmp2)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = canonical(tmp1); err == nil {
|
||||
t.Fatalf("want canonical(%q)!=nil", tmp1)
|
||||
}
|
||||
if _, ok := err.(*os.PathError); !ok {
|
||||
t.Fatalf("want canonical(%q)=os.PathError; got %T", tmp1, err)
|
||||
}
|
||||
}
|
||||
|
||||
// issue #83
|
||||
func TestCanonical_RelativeSymlink(t *testing.T) {
|
||||
dir, err := ioutil.TempDir(wd, "")
|
||||
if err != nil {
|
||||
t.Fatalf("TempDir()=%v", err)
|
||||
}
|
||||
var (
|
||||
path = filepath.Join(dir, filepath.FromSlash("a/b/c/d/e/f"))
|
||||
realpath = filepath.Join(dir, filepath.FromSlash("a/b/x/y/z/d/e/f"))
|
||||
rel = filepath.FromSlash("x/y/z/../z/../z")
|
||||
chdir = filepath.Join(dir, filepath.FromSlash("a/b"))
|
||||
)
|
||||
defer os.RemoveAll(dir)
|
||||
if err = os.MkdirAll(realpath, 0755); err != nil {
|
||||
t.Fatalf("MkdirAll()=%v", err)
|
||||
}
|
||||
if err := os.Chdir(chdir); err != nil {
|
||||
t.Fatalf("Chdir()=%v", err)
|
||||
}
|
||||
if err := nonil(os.Symlink(rel, "c"), os.Chdir(wd)); err != nil {
|
||||
t.Fatalf("Symlink()=%v", err)
|
||||
}
|
||||
got, err := canonical(path)
|
||||
if err != nil {
|
||||
t.Fatalf("canonical(%s)=%v", path, err)
|
||||
}
|
||||
if got != realpath {
|
||||
t.Fatalf("want canonical()=%s; got %s", realpath, got)
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package notify
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
errAlreadyWatched = errors.New("path is already watched")
|
||||
errNotWatched = errors.New("path is not being watched")
|
||||
errInvalidEventSet = errors.New("invalid event set provided")
|
||||
)
|
||||
|
||||
// Watcher is a intermediate interface for wrapping inotify, ReadDirChangesW,
|
||||
// FSEvents, kqueue and poller implementations.
|
||||
//
|
||||
// The watcher implementation is expected to do its own mapping between paths and
|
||||
// create watchers if underlying event notification does not support it. For
|
||||
// the ease of implementation it is guaranteed that paths provided via Watch and
|
||||
// Unwatch methods are absolute and clean.
|
||||
type watcher interface {
|
||||
// Watch requests a watcher creation for the given path and given event set.
|
||||
Watch(path string, event Event) error
|
||||
|
||||
// Unwatch requests a watcher deletion for the given path and given event set.
|
||||
Unwatch(path string) error
|
||||
|
||||
// Rewatch provides a functionality for modifying existing watch-points, like
|
||||
// expanding its event set.
|
||||
//
|
||||
// Rewatch modifies existing watch-point under for the given path. It passes
|
||||
// the existing event set currently registered for the given path, and the
|
||||
// new, requested event set.
|
||||
//
|
||||
// It is guaranteed that Tree will not pass to Rewatch zero value for any
|
||||
// of its arguments. If old == new and watcher can be upgraded to
|
||||
// recursiveWatcher interface, a watch for the corresponding path is expected
|
||||
// to be changed from recursive to the non-recursive one.
|
||||
Rewatch(path string, old, new Event) error
|
||||
|
||||
// Close unwatches all paths that are registered. When Close returns, it
|
||||
// is expected it will report no more events.
|
||||
Close() error
|
||||
}
|
||||
|
||||
// RecursiveWatcher is an interface for a Watcher for those OS, which do support
|
||||
// recursive watching over directories.
|
||||
type recursiveWatcher interface {
|
||||
RecursiveWatch(path string, event Event) error
|
||||
|
||||
// RecursiveUnwatch removes a recursive watch-point given by the path. For
|
||||
// native recursive implementation there is no difference in functionality
|
||||
// between Unwatch and RecursiveUnwatch, however for those platforms, that
|
||||
// requires emulation for recursive watch-points, the implementation differs.
|
||||
RecursiveUnwatch(path string) error
|
||||
|
||||
// RecursiveRewatcher provides a functionality for modifying and/or relocating
|
||||
// existing recursive watch-points.
|
||||
//
|
||||
// To relocate a watch-point means to unwatch oldpath and set a watch-point on
|
||||
// newpath.
|
||||
//
|
||||
// To modify a watch-point means either to expand or shrink its event set.
|
||||
//
|
||||
// Tree can want to either relocate, modify or relocate and modify a watch-point
|
||||
// via single RecursiveRewatch call.
|
||||
//
|
||||
// If oldpath == newpath, the watch-point is expected to change its event set value
|
||||
// from oldevent to newevent.
|
||||
//
|
||||
// If oldevent == newevent, the watch-point is expected to relocate from oldpath
|
||||
// to the newpath.
|
||||
//
|
||||
// If oldpath != newpath and oldevent != newevent, the watch-point is expected
|
||||
// to relocate from oldpath to the newpath first and then change its event set
|
||||
// value from oldevent to the newevent. In other words the end result must be
|
||||
// a watch-point set on newpath with newevent value of its event set.
|
||||
//
|
||||
// It is guaranteed that Tree will not pass to RecurisveRewatcha zero value
|
||||
// for any of its arguments. If oldpath == newpath and oldevent == newevent,
|
||||
// a watch for the corresponding path is expected to be changed for
|
||||
// non-recursive to the recursive one.
|
||||
RecursiveRewatch(oldpath, newpath string, oldevent, newevent Event) error
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build solaris
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// newTrigger returns implementation of trigger.
|
||||
func newTrigger(pthLkp map[string]*watched) trigger {
|
||||
return &fen{
|
||||
pthLkp: pthLkp,
|
||||
cf: newCfen(),
|
||||
}
|
||||
}
|
||||
|
||||
// fen is a structure implementing trigger for FEN.
|
||||
type fen struct {
|
||||
// p is a FEN port identifier
|
||||
p int
|
||||
// pthLkp is a structure mapping monitored files/dir with data about them,
|
||||
// shared with parent trg structure
|
||||
pthLkp map[string]*watched
|
||||
// cf wraps C operations for FEN
|
||||
cf cfen
|
||||
}
|
||||
|
||||
// watched is a data structure representing watched file/directory.
|
||||
type watched struct {
|
||||
trgWatched
|
||||
}
|
||||
|
||||
// Stop implements trigger.
|
||||
func (f *fen) Stop() error {
|
||||
return f.cf.portAlert(f.p)
|
||||
}
|
||||
|
||||
// Close implements trigger.
|
||||
func (f *fen) Close() (err error) {
|
||||
return syscall.Close(f.p)
|
||||
}
|
||||
|
||||
// NewWatched implements trigger.
|
||||
func (*fen) NewWatched(p string, fi os.FileInfo) (*watched, error) {
|
||||
return &watched{trgWatched{p: p, fi: fi}}, nil
|
||||
}
|
||||
|
||||
// Record implements trigger.
|
||||
func (f *fen) Record(w *watched) {
|
||||
f.pthLkp[w.p] = w
|
||||
}
|
||||
|
||||
// Del implements trigger.
|
||||
func (f *fen) Del(w *watched) {
|
||||
delete(f.pthLkp, w.p)
|
||||
}
|
||||
|
||||
func inter2pe(n interface{}) PortEvent {
|
||||
pe, ok := n.(PortEvent)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("fen: type should be PortEvent, %T instead", n))
|
||||
}
|
||||
return pe
|
||||
}
|
||||
|
||||
// Watched implements trigger.
|
||||
func (f *fen) Watched(n interface{}) (*watched, int64, error) {
|
||||
pe := inter2pe(n)
|
||||
fo, ok := pe.PortevObject.(*FileObj)
|
||||
if !ok || fo == nil {
|
||||
panic(fmt.Sprintf("fen: type should be *FileObj, %T instead", fo))
|
||||
}
|
||||
w, ok := f.pthLkp[fo.Name]
|
||||
if !ok {
|
||||
return nil, 0, errNotWatched
|
||||
}
|
||||
return w, int64(pe.PortevEvents), nil
|
||||
}
|
||||
|
||||
// init initializes FEN.
|
||||
func (f *fen) Init() (err error) {
|
||||
f.p, err = f.cf.portCreate()
|
||||
return
|
||||
}
|
||||
|
||||
func fi2fo(fi os.FileInfo, p string) FileObj {
|
||||
st, ok := fi.Sys().(*syscall.Stat_t)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("fen: type should be *syscall.Stat_t, %T instead", st))
|
||||
}
|
||||
return FileObj{Name: p, Atim: st.Atim, Mtim: st.Mtim, Ctim: st.Ctim}
|
||||
}
|
||||
|
||||
// Unwatch implements trigger.
|
||||
func (f *fen) Unwatch(w *watched) error {
|
||||
return f.cf.portDissociate(f.p, FileObj{Name: w.p})
|
||||
}
|
||||
|
||||
// Watch implements trigger.
|
||||
func (f *fen) Watch(fi os.FileInfo, w *watched, e int64) error {
|
||||
return f.cf.portAssociate(f.p, fi2fo(fi, w.p), int(e))
|
||||
}
|
||||
|
||||
// Wait implements trigger.
|
||||
func (f *fen) Wait() (interface{}, error) {
|
||||
var (
|
||||
pe PortEvent
|
||||
err error
|
||||
)
|
||||
err = f.cf.portGet(f.p, &pe)
|
||||
return pe, err
|
||||
}
|
||||
|
||||
// IsStop implements trigger.
|
||||
func (f *fen) IsStop(n interface{}, err error) bool {
|
||||
return err == syscall.EBADF || inter2pe(n).PortevSource == srcAlert
|
||||
}
|
||||
|
||||
func init() {
|
||||
encode = func(e Event, dir bool) (o int64) {
|
||||
// Create event is not supported by FEN. Instead FileModified event will
|
||||
// be registered. If this event will be reported on dir which is to be
|
||||
// monitored for Create, dir will be rescanned and Create events will
|
||||
// be generated and returned for new files. In case of files,
|
||||
// if not requested FileModified event is reported, it will be ignored.
|
||||
o = int64(e &^ Create)
|
||||
if (e&Create != 0 && dir) || e&Write != 0 {
|
||||
o = (o &^ int64(Write)) | int64(FileModified)
|
||||
}
|
||||
// Following events are 'exception events' and as such cannot be requested
|
||||
// explicitly for monitoring or filtered out. If the will be reported
|
||||
// by FEN and not subscribed with by user, they will be filtered out by
|
||||
// watcher's logic.
|
||||
o &= int64(^Rename & ^Remove &^ FileDelete &^ FileRenameTo &^
|
||||
FileRenameFrom &^ Unmounted &^ MountedOver)
|
||||
return
|
||||
}
|
||||
nat2not = map[Event]Event{
|
||||
FileModified: Write,
|
||||
FileRenameFrom: Rename,
|
||||
FileDelete: Remove,
|
||||
FileAccess: Event(0),
|
||||
FileAttrib: Event(0),
|
||||
FileRenameTo: Event(0),
|
||||
FileTrunc: Event(0),
|
||||
FileNoFollow: Event(0),
|
||||
Unmounted: Event(0),
|
||||
MountedOver: Event(0),
|
||||
}
|
||||
not2nat = map[Event]Event{
|
||||
Write: FileModified,
|
||||
Rename: FileRenameFrom,
|
||||
Remove: FileDelete,
|
||||
}
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build solaris
|
||||
|
||||
package notify
|
||||
|
||||
// #include <port.h>
|
||||
// #include <stdio.h>
|
||||
// #include <stdlib.h>
|
||||
// struct file_obj* newFo() { return (struct file_obj*) malloc(sizeof(struct file_obj)); }
|
||||
// port_event_t* newPe() { return (port_event_t*) malloc(sizeof(port_event_t)); }
|
||||
// uintptr_t conv(struct file_obj* fo) { return (uintptr_t) fo; }
|
||||
// struct file_obj* dconv(uintptr_t fo) { return (struct file_obj*) fo; }
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
fileAccess = Event(C.FILE_ACCESS)
|
||||
fileModified = Event(C.FILE_MODIFIED)
|
||||
fileAttrib = Event(C.FILE_ATTRIB)
|
||||
fileDelete = Event(C.FILE_DELETE)
|
||||
fileRenameTo = Event(C.FILE_RENAME_TO)
|
||||
fileRenameFrom = Event(C.FILE_RENAME_FROM)
|
||||
fileTrunc = Event(C.FILE_TRUNC)
|
||||
fileNoFollow = Event(C.FILE_NOFOLLOW)
|
||||
unmounted = Event(C.UNMOUNTED)
|
||||
mountedOver = Event(C.MOUNTEDOVER)
|
||||
)
|
||||
|
||||
// PortEvent is a notify's equivalent of port_event_t.
|
||||
type PortEvent struct {
|
||||
PortevEvents int // PortevEvents is an equivalent of portev_events.
|
||||
PortevSource uint8 // PortevSource is an equivalent of portev_source.
|
||||
PortevPad uint8 // Portevpad is an equivalent of portev_pad.
|
||||
PortevObject interface{} // PortevObject is an equivalent of portev_object.
|
||||
PortevUser uintptr // PortevUser is an equivalent of portev_user.
|
||||
}
|
||||
|
||||
// FileObj is a notify's equivalent of file_obj.
|
||||
type FileObj struct {
|
||||
Atim syscall.Timespec // Atim is an equivalent of fo_atime.
|
||||
Mtim syscall.Timespec // Mtim is an equivalent of fo_mtime.
|
||||
Ctim syscall.Timespec // Ctim is an equivalent of fo_ctime.
|
||||
Pad [3]uintptr // Pad is an equivalent of fo_pad.
|
||||
Name string // Name is an equivalent of fo_name.
|
||||
}
|
||||
|
||||
type cfen struct {
|
||||
p2pe map[string]*C.port_event_t
|
||||
p2fo map[string]*C.struct_file_obj
|
||||
}
|
||||
|
||||
func newCfen() cfen {
|
||||
return cfen{
|
||||
p2pe: make(map[string]*C.port_event_t),
|
||||
p2fo: make(map[string]*C.struct_file_obj),
|
||||
}
|
||||
}
|
||||
|
||||
func unix2C(sec int64, nsec int64) (C.time_t, C.long) {
|
||||
return C.time_t(sec), C.long(nsec)
|
||||
}
|
||||
|
||||
func (c *cfen) portAssociate(p int, fo FileObj, e int) (err error) {
|
||||
cfo := C.newFo()
|
||||
cfo.fo_atime.tv_sec, cfo.fo_atime.tv_nsec = unix2C(fo.Atim.Unix())
|
||||
cfo.fo_mtime.tv_sec, cfo.fo_mtime.tv_nsec = unix2C(fo.Mtim.Unix())
|
||||
cfo.fo_ctime.tv_sec, cfo.fo_ctime.tv_nsec = unix2C(fo.Ctim.Unix())
|
||||
cfo.fo_name = C.CString(fo.Name)
|
||||
c.p2fo[fo.Name] = cfo
|
||||
_, err = C.port_associate(C.int(p), srcFile, C.conv(cfo), C.int(e), nil)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *cfen) portDissociate(port int, fo FileObj) (err error) {
|
||||
cfo, ok := c.p2fo[fo.Name]
|
||||
if !ok {
|
||||
return errNotWatched
|
||||
}
|
||||
_, err = C.port_dissociate(C.int(port), srcFile, C.conv(cfo))
|
||||
C.free(unsafe.Pointer(cfo.fo_name))
|
||||
C.free(unsafe.Pointer(cfo))
|
||||
delete(c.p2fo, fo.Name)
|
||||
return
|
||||
}
|
||||
|
||||
const srcAlert = C.PORT_SOURCE_ALERT
|
||||
const srcFile = C.PORT_SOURCE_FILE
|
||||
const alertSet = C.PORT_ALERT_SET
|
||||
|
||||
func cfo2fo(cfo *C.struct_file_obj) *FileObj {
|
||||
// Currently remaining attributes are not used.
|
||||
if cfo == nil {
|
||||
return nil
|
||||
}
|
||||
var fo FileObj
|
||||
fo.Name = C.GoString(cfo.fo_name)
|
||||
return &fo
|
||||
}
|
||||
|
||||
func (c *cfen) portGet(port int, pe *PortEvent) (err error) {
|
||||
cpe := C.newPe()
|
||||
if _, err = C.port_get(C.int(port), cpe, nil); err != nil {
|
||||
C.free(unsafe.Pointer(cpe))
|
||||
return
|
||||
}
|
||||
pe.PortevEvents, pe.PortevSource, pe.PortevPad =
|
||||
int(cpe.portev_events), uint8(cpe.portev_source), uint8(cpe.portev_pad)
|
||||
pe.PortevObject = cfo2fo(C.dconv(cpe.portev_object))
|
||||
pe.PortevUser = uintptr(cpe.portev_user)
|
||||
C.free(unsafe.Pointer(cpe))
|
||||
return
|
||||
}
|
||||
|
||||
func (c *cfen) portCreate() (int, error) {
|
||||
p, err := C.port_create()
|
||||
return int(p), err
|
||||
}
|
||||
|
||||
func (c *cfen) portAlert(p int) (err error) {
|
||||
_, err = C.port_alert(C.int(p), alertSet, C.int(666), nil)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *cfen) free() {
|
||||
for i := range c.p2fo {
|
||||
C.free(unsafe.Pointer(c.p2fo[i].fo_name))
|
||||
C.free(unsafe.Pointer(c.p2fo[i]))
|
||||
}
|
||||
for i := range c.p2pe {
|
||||
C.free(unsafe.Pointer(c.p2pe[i]))
|
||||
}
|
||||
c.p2fo = make(map[string]*C.struct_file_obj)
|
||||
c.p2pe = make(map[string]*C.port_event_t)
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build solaris
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func fremove(w *W, path string, files []string) WCase {
|
||||
cas := remove(w, path)
|
||||
cas.Events[0] = &Call{P: path, E: FileDelete}
|
||||
for _, f := range files {
|
||||
cas.Events = append(cas.Events, &Call{P: f, E: FileDelete})
|
||||
}
|
||||
return cas
|
||||
}
|
||||
|
||||
func fwrite(w *W, path string, p []byte) WCase {
|
||||
cas := write(w, path, p)
|
||||
path = cas.Events[0].Path()
|
||||
cas.Events[0] = &Call{P: path, E: FileModified}
|
||||
return cas
|
||||
}
|
||||
|
||||
func frename(w *W, path string, files []string) WCase {
|
||||
const ext = ".notify"
|
||||
cas := WCase{
|
||||
Action: func() {
|
||||
file := filepath.Join(w.root, path)
|
||||
if err := os.Rename(file, file+ext); err != nil {
|
||||
w.Fatalf("Rename(%q, %q)=%v", path, path+ext, err)
|
||||
}
|
||||
},
|
||||
Events: []EventInfo{
|
||||
&Call{P: path + ext, E: osSpecificCreate},
|
||||
&Call{P: path, E: FileRenameFrom},
|
||||
},
|
||||
}
|
||||
for _, f := range files {
|
||||
cas.Events = append(cas.Events, &Call{P: f, E: FileRenameFrom})
|
||||
}
|
||||
return cas
|
||||
}
|
||||
|
||||
var events = []Event{
|
||||
FileModified,
|
||||
FileAttrib,
|
||||
FileRenameFrom,
|
||||
osSpecificCreate,
|
||||
FileDelete,
|
||||
}
|
||||
|
||||
func TestWatcherFen(t *testing.T) {
|
||||
w := NewWatcherTest(t, "testdata/vfs.txt", events...)
|
||||
defer w.Close()
|
||||
|
||||
cases := [...]WCase{
|
||||
fremove(w, "src/github.com/ppknap/link/include/coost/link", []string{
|
||||
"src/github.com/ppknap/link/include/coost/link/definitions.hpp",
|
||||
"src/github.com/ppknap/link/include/coost/link/detail/bundle.hpp",
|
||||
"src/github.com/ppknap/link/include/coost/link/detail/container_invoker.hpp",
|
||||
"src/github.com/ppknap/link/include/coost/link/detail/container_value_trait.hpp",
|
||||
"src/github.com/ppknap/link/include/coost/link/detail/dummy_type.hpp",
|
||||
"src/github.com/ppknap/link/include/coost/link/detail/function_trait.hpp",
|
||||
"src/github.com/ppknap/link/include/coost/link/detail/immediate_invoker.hpp",
|
||||
"src/github.com/ppknap/link/include/coost/link/detail/stdhelpers/always_same.hpp",
|
||||
"src/github.com/ppknap/link/include/coost/link/detail/stdhelpers/make_unique.hpp",
|
||||
"src/github.com/ppknap/link/include/coost/link/detail/stdhelpers",
|
||||
"src/github.com/ppknap/link/include/coost/link/detail/vertex.hpp",
|
||||
"src/github.com/ppknap/link/include/coost/link/detail/wire.hpp",
|
||||
"src/github.com/ppknap/link/include/coost/link/detail",
|
||||
"src/github.com/ppknap/link/include/coost/link/link.hpp",
|
||||
},
|
||||
),
|
||||
fwrite(w, "src/github.com/rjeczalik/fs/fs.go", []byte("XD")),
|
||||
fremove(w, "src/github.com/ppknap/link/README.md", nil),
|
||||
frename(w, "src/github.com/rjeczalik/fs/fs.go", nil),
|
||||
frename(w, "src/github.com/rjeczalik/fs/cmd/gotree", []string{
|
||||
"src/github.com/rjeczalik/fs/cmd/gotree/go.go",
|
||||
"src/github.com/rjeczalik/fs/cmd/gotree/main.go",
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
w.ExpectAll(cases[:])
|
||||
}
|
||||
+311
@@ -0,0 +1,311 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build darwin,!kqueue
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
const (
|
||||
failure = uint32(FSEventsMustScanSubDirs | FSEventsUserDropped | FSEventsKernelDropped)
|
||||
filter = uint32(FSEventsCreated | FSEventsRemoved | FSEventsRenamed |
|
||||
FSEventsModified | FSEventsInodeMetaMod)
|
||||
)
|
||||
|
||||
// FSEvent represents single file event. It is created out of values passed by
|
||||
// FSEvents to FSEventStreamCallback function.
|
||||
type FSEvent struct {
|
||||
Path string // real path of the file or directory
|
||||
ID uint64 // ID of the event (FSEventStreamEventId)
|
||||
Flags uint32 // joint FSEvents* flags (FSEventStreamEventFlags)
|
||||
}
|
||||
|
||||
// splitflags separates event flags from single set into slice of flags.
|
||||
func splitflags(set uint32) (e []uint32) {
|
||||
for i := uint32(1); set != 0; i, set = i<<1, set>>1 {
|
||||
if (set & 1) != 0 {
|
||||
e = append(e, i)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// watch represents a filesystem watchpoint. It is a higher level abstraction
|
||||
// over FSEvents' stream, which implements filtering of file events based
|
||||
// on path and event set. It emulates non-recursive watch-point by filtering out
|
||||
// events which paths are more than 1 level deeper than the watched path.
|
||||
type watch struct {
|
||||
// prev stores last event set per path in order to filter out old flags
|
||||
// for new events, which appratenly FSEvents likes to retain. It's a disgusting
|
||||
// hack, it should be researched how to get rid of it.
|
||||
prev map[string]uint32
|
||||
c chan<- EventInfo
|
||||
stream *stream
|
||||
path string
|
||||
events uint32
|
||||
isrec int32
|
||||
flushed bool
|
||||
}
|
||||
|
||||
// Example format:
|
||||
//
|
||||
// ~ $ (trigger command) # (event set) -> (effective event set)
|
||||
//
|
||||
// Heuristics:
|
||||
//
|
||||
// 1. Create event is removed when it was present in previous event set.
|
||||
// Example:
|
||||
//
|
||||
// ~ $ echo > file # Create|Write -> Create|Write
|
||||
// ~ $ echo > file # Create|Write|InodeMetaMod -> Write|InodeMetaMod
|
||||
//
|
||||
// 2. Remove event is removed if it was present in previouse event set.
|
||||
// Example:
|
||||
//
|
||||
// ~ $ touch file # Create -> Create
|
||||
// ~ $ rm file # Create|Remove -> Remove
|
||||
// ~ $ touch file # Create|Remove -> Create
|
||||
//
|
||||
// 3. Write event is removed if not followed by InodeMetaMod on existing
|
||||
// file. Example:
|
||||
//
|
||||
// ~ $ echo > file # Create|Write -> Create|Write
|
||||
// ~ $ chmod +x file # Create|Write|ChangeOwner -> ChangeOwner
|
||||
//
|
||||
// 4. Write&InodeMetaMod is removed when effective event set contain Remove event.
|
||||
// Example:
|
||||
//
|
||||
// ~ $ echo > file # Write|InodeMetaMod -> Write|InodeMetaMod
|
||||
// ~ $ rm file # Remove|Write|InodeMetaMod -> Remove
|
||||
//
|
||||
func (w *watch) strip(base string, set uint32) uint32 {
|
||||
const (
|
||||
write = FSEventsModified | FSEventsInodeMetaMod
|
||||
both = FSEventsCreated | FSEventsRemoved
|
||||
)
|
||||
switch w.prev[base] {
|
||||
case FSEventsCreated:
|
||||
set &^= FSEventsCreated
|
||||
if set&FSEventsRemoved != 0 {
|
||||
w.prev[base] = FSEventsRemoved
|
||||
set &^= write
|
||||
}
|
||||
case FSEventsRemoved:
|
||||
set &^= FSEventsRemoved
|
||||
if set&FSEventsCreated != 0 {
|
||||
w.prev[base] = FSEventsCreated
|
||||
}
|
||||
default:
|
||||
switch set & both {
|
||||
case FSEventsCreated:
|
||||
w.prev[base] = FSEventsCreated
|
||||
case FSEventsRemoved:
|
||||
w.prev[base] = FSEventsRemoved
|
||||
set &^= write
|
||||
}
|
||||
}
|
||||
dbgprintf("split()=%v\n", Event(set))
|
||||
return set
|
||||
}
|
||||
|
||||
// Dispatch is a stream function which forwards given file events for the watched
|
||||
// path to underlying FileInfo channel.
|
||||
func (w *watch) Dispatch(ev []FSEvent) {
|
||||
events := atomic.LoadUint32(&w.events)
|
||||
isrec := (atomic.LoadInt32(&w.isrec) == 1)
|
||||
for i := range ev {
|
||||
if ev[i].Flags&FSEventsHistoryDone != 0 {
|
||||
w.flushed = true
|
||||
continue
|
||||
}
|
||||
if !w.flushed {
|
||||
continue
|
||||
}
|
||||
dbgprintf("%v (0x%x) (%s, i=%d, ID=%d, len=%d)\n", Event(ev[i].Flags),
|
||||
ev[i].Flags, ev[i].Path, i, ev[i].ID, len(ev))
|
||||
if ev[i].Flags&failure != 0 {
|
||||
// TODO(rjeczalik): missing error handling
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(ev[i].Path, w.path) {
|
||||
continue
|
||||
}
|
||||
n := len(w.path)
|
||||
base := ""
|
||||
if len(ev[i].Path) > n {
|
||||
if ev[i].Path[n] != '/' {
|
||||
continue
|
||||
}
|
||||
base = ev[i].Path[n+1:]
|
||||
if !isrec && strings.IndexByte(base, '/') != -1 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
// TODO(rjeczalik): get diff only from filtered events?
|
||||
e := w.strip(string(base), ev[i].Flags) & events
|
||||
if e == 0 {
|
||||
continue
|
||||
}
|
||||
for _, e := range splitflags(e) {
|
||||
dbgprintf("%d: single event: %v", ev[i].ID, Event(e))
|
||||
w.c <- &event{
|
||||
fse: ev[i],
|
||||
event: Event(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop closes underlying FSEvents stream and stops dispatching events.
|
||||
func (w *watch) Stop() {
|
||||
w.stream.Stop()
|
||||
// TODO(rjeczalik): make (*stream).Stop flush synchronously undelivered events,
|
||||
// so the following hack can be removed. It should flush all the streams
|
||||
// concurrently as we care not to block too much here.
|
||||
atomic.StoreUint32(&w.events, 0)
|
||||
atomic.StoreInt32(&w.isrec, 0)
|
||||
}
|
||||
|
||||
// fsevents implements Watcher and RecursiveWatcher interfaces backed by FSEvents
|
||||
// framework.
|
||||
type fsevents struct {
|
||||
watches map[string]*watch
|
||||
c chan<- EventInfo
|
||||
}
|
||||
|
||||
func newWatcher(c chan<- EventInfo) watcher {
|
||||
return &fsevents{
|
||||
watches: make(map[string]*watch),
|
||||
c: c,
|
||||
}
|
||||
}
|
||||
|
||||
func (fse *fsevents) watch(path string, event Event, isrec int32) (err error) {
|
||||
if _, ok := fse.watches[path]; ok {
|
||||
return errAlreadyWatched
|
||||
}
|
||||
w := &watch{
|
||||
prev: make(map[string]uint32),
|
||||
c: fse.c,
|
||||
path: path,
|
||||
events: uint32(event),
|
||||
isrec: isrec,
|
||||
}
|
||||
w.stream = newStream(path, w.Dispatch)
|
||||
if err = w.stream.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
fse.watches[path] = w
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fse *fsevents) unwatch(path string) (err error) {
|
||||
w, ok := fse.watches[path]
|
||||
if !ok {
|
||||
return errNotWatched
|
||||
}
|
||||
w.stream.Stop()
|
||||
delete(fse.watches, path)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Watch implements Watcher interface. It fails with non-nil error when setting
|
||||
// the watch-point by FSEvents fails or with errAlreadyWatched error when
|
||||
// the given path is already watched.
|
||||
func (fse *fsevents) Watch(path string, event Event) error {
|
||||
return fse.watch(path, event, 0)
|
||||
}
|
||||
|
||||
// Unwatch implements Watcher interface. It fails with errNotWatched when
|
||||
// the given path is not being watched.
|
||||
func (fse *fsevents) Unwatch(path string) error {
|
||||
return fse.unwatch(path)
|
||||
}
|
||||
|
||||
// Rewatch implements Watcher interface. It fails with errNotWatched when
|
||||
// the given path is not being watched or with errInvalidEventSet when oldevent
|
||||
// does not match event set the watch-point currently holds.
|
||||
func (fse *fsevents) Rewatch(path string, oldevent, newevent Event) error {
|
||||
w, ok := fse.watches[path]
|
||||
if !ok {
|
||||
return errNotWatched
|
||||
}
|
||||
if !atomic.CompareAndSwapUint32(&w.events, uint32(oldevent), uint32(newevent)) {
|
||||
return errInvalidEventSet
|
||||
}
|
||||
atomic.StoreInt32(&w.isrec, 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RecursiveWatch implements RecursiveWatcher interface. It fails with non-nil
|
||||
// error when setting the watch-point by FSEvents fails or with errAlreadyWatched
|
||||
// error when the given path is already watched.
|
||||
func (fse *fsevents) RecursiveWatch(path string, event Event) error {
|
||||
return fse.watch(path, event, 1)
|
||||
}
|
||||
|
||||
// RecursiveUnwatch implements RecursiveWatcher interface. It fails with
|
||||
// errNotWatched when the given path is not being watched.
|
||||
//
|
||||
// TODO(rjeczalik): fail if w.isrec == 0?
|
||||
func (fse *fsevents) RecursiveUnwatch(path string) error {
|
||||
return fse.unwatch(path)
|
||||
}
|
||||
|
||||
// RecrusiveRewatch implements RecursiveWatcher interface. It fails:
|
||||
//
|
||||
// * with errNotWatched when the given path is not being watched
|
||||
// * with errInvalidEventSet when oldevent does not match the current event set
|
||||
// * with errAlreadyWatched when watch-point given by the oldpath was meant to
|
||||
// be relocated to newpath, but the newpath is already watched
|
||||
// * a non-nil error when setting the watch-point with FSEvents fails
|
||||
//
|
||||
// TODO(rjeczalik): Improve handling of watch-point relocation? See two TODOs
|
||||
// that follows.
|
||||
func (fse *fsevents) RecursiveRewatch(oldpath, newpath string, oldevent, newevent Event) error {
|
||||
switch [2]bool{oldpath == newpath, oldevent == newevent} {
|
||||
case [2]bool{true, true}:
|
||||
w, ok := fse.watches[oldpath]
|
||||
if !ok {
|
||||
return errNotWatched
|
||||
}
|
||||
atomic.StoreInt32(&w.isrec, 1)
|
||||
return nil
|
||||
case [2]bool{true, false}:
|
||||
w, ok := fse.watches[oldpath]
|
||||
if !ok {
|
||||
return errNotWatched
|
||||
}
|
||||
if !atomic.CompareAndSwapUint32(&w.events, uint32(oldevent), uint32(newevent)) {
|
||||
return errors.New("invalid event state diff")
|
||||
}
|
||||
atomic.StoreInt32(&w.isrec, 1)
|
||||
return nil
|
||||
default:
|
||||
// TODO(rjeczalik): rewatch newpath only if exists?
|
||||
// TODO(rjeczalik): migrate w.prev to new watch?
|
||||
if _, ok := fse.watches[newpath]; ok {
|
||||
return errAlreadyWatched
|
||||
}
|
||||
if err := fse.Unwatch(oldpath); err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO(rjeczalik): revert unwatch if watch fails?
|
||||
return fse.watch(newpath, newevent, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Close unwatches all watch-points.
|
||||
func (fse *fsevents) Close() error {
|
||||
for _, w := range fse.watches {
|
||||
w.Stop()
|
||||
}
|
||||
fse.watches = nil
|
||||
return nil
|
||||
}
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build darwin,!kqueue
|
||||
|
||||
package notify
|
||||
|
||||
/*
|
||||
#include <CoreServices/CoreServices.h>
|
||||
|
||||
typedef void (*CFRunLoopPerformCallBack)(void*);
|
||||
|
||||
void gosource(void *);
|
||||
void gostream(uintptr_t, uintptr_t, size_t, uintptr_t, uintptr_t, uintptr_t);
|
||||
|
||||
static FSEventStreamRef EventStreamCreate(FSEventStreamContext * context, uintptr_t info, CFArrayRef paths, FSEventStreamEventId since, CFTimeInterval latency, FSEventStreamCreateFlags flags) {
|
||||
context->info = (void*) info;
|
||||
return FSEventStreamCreate(NULL, (FSEventStreamCallback) gostream, context, paths, since, latency, flags);
|
||||
}
|
||||
|
||||
#cgo LDFLAGS: -framework CoreServices
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var nilstream C.FSEventStreamRef
|
||||
|
||||
// Default arguments for FSEventStreamCreate function.
|
||||
var (
|
||||
latency C.CFTimeInterval
|
||||
flags = C.FSEventStreamCreateFlags(C.kFSEventStreamCreateFlagFileEvents | C.kFSEventStreamCreateFlagNoDefer)
|
||||
since = uint64(C.FSEventsGetCurrentEventId())
|
||||
)
|
||||
|
||||
var runloop C.CFRunLoopRef // global runloop which all streams are registered with
|
||||
var wg sync.WaitGroup // used to wait until the runloop starts
|
||||
|
||||
// source is used for synchronization purposes - it signals when runloop has
|
||||
// started and is ready via the wg. It also serves purpose of a dummy source,
|
||||
// thanks to it the runloop does not return as it also has at least one source
|
||||
// registered.
|
||||
var source = C.CFRunLoopSourceCreate(nil, 0, &C.CFRunLoopSourceContext{
|
||||
perform: (C.CFRunLoopPerformCallBack)(C.gosource),
|
||||
})
|
||||
|
||||
// Errors returned when FSEvents functions fail.
|
||||
var (
|
||||
errCreate = os.NewSyscallError("FSEventStreamCreate", errors.New("NULL"))
|
||||
errStart = os.NewSyscallError("FSEventStreamStart", errors.New("false"))
|
||||
)
|
||||
|
||||
// initializes the global runloop and ensures any created stream awaits its
|
||||
// readiness.
|
||||
func init() {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
// There is exactly one run loop per thread. Lock this goroutine to its
|
||||
// thread to ensure that it's not rescheduled on a different thread while
|
||||
// setting up the run loop.
|
||||
runtime.LockOSThread()
|
||||
runloop = C.CFRunLoopGetCurrent()
|
||||
C.CFRunLoopAddSource(runloop, source, C.kCFRunLoopDefaultMode)
|
||||
C.CFRunLoopRun()
|
||||
panic("runloop has just unexpectedly stopped")
|
||||
}()
|
||||
C.CFRunLoopSourceSignal(source)
|
||||
}
|
||||
|
||||
//export gosource
|
||||
func gosource(unsafe.Pointer) {
|
||||
wg.Done()
|
||||
}
|
||||
|
||||
//export gostream
|
||||
func gostream(_, info uintptr, n C.size_t, paths, flags, ids uintptr) {
|
||||
const (
|
||||
offchar = unsafe.Sizeof((*C.char)(nil))
|
||||
offflag = unsafe.Sizeof(C.FSEventStreamEventFlags(0))
|
||||
offid = unsafe.Sizeof(C.FSEventStreamEventId(0))
|
||||
)
|
||||
if n == 0 {
|
||||
return
|
||||
}
|
||||
fn := streamFuncs.get(info)
|
||||
if fn == nil {
|
||||
return
|
||||
}
|
||||
ev := make([]FSEvent, 0, int(n))
|
||||
for i := uintptr(0); i < uintptr(n); i++ {
|
||||
switch flags := *(*uint32)(unsafe.Pointer((flags + i*offflag))); {
|
||||
case flags&uint32(FSEventsEventIdsWrapped) != 0:
|
||||
atomic.StoreUint64(&since, uint64(C.FSEventsGetCurrentEventId()))
|
||||
default:
|
||||
ev = append(ev, FSEvent{
|
||||
Path: C.GoString(*(**C.char)(unsafe.Pointer(paths + i*offchar))),
|
||||
Flags: flags,
|
||||
ID: *(*uint64)(unsafe.Pointer(ids + i*offid)),
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
fn(ev)
|
||||
}
|
||||
|
||||
// StreamFunc is a callback called when stream receives file events.
|
||||
type streamFunc func([]FSEvent)
|
||||
|
||||
var streamFuncs = streamFuncRegistry{m: map[uintptr]streamFunc{}}
|
||||
|
||||
type streamFuncRegistry struct {
|
||||
mu sync.Mutex
|
||||
m map[uintptr]streamFunc
|
||||
i uintptr
|
||||
}
|
||||
|
||||
func (r *streamFuncRegistry) get(id uintptr) streamFunc {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.m[id]
|
||||
}
|
||||
|
||||
func (r *streamFuncRegistry) add(fn streamFunc) uintptr {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.i++
|
||||
r.m[r.i] = fn
|
||||
return r.i
|
||||
}
|
||||
|
||||
func (r *streamFuncRegistry) delete(id uintptr) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
delete(r.m, id)
|
||||
}
|
||||
|
||||
// Stream represents single watch-point which listens for events scheduled by
|
||||
// the global runloop.
|
||||
type stream struct {
|
||||
path string
|
||||
ref C.FSEventStreamRef
|
||||
info uintptr
|
||||
}
|
||||
|
||||
// NewStream creates a stream for given path, listening for file events and
|
||||
// calling fn upon receiving any.
|
||||
func newStream(path string, fn streamFunc) *stream {
|
||||
return &stream{
|
||||
path: path,
|
||||
info: streamFuncs.add(fn),
|
||||
}
|
||||
}
|
||||
|
||||
// Start creates a FSEventStream for the given path and schedules it with
|
||||
// global runloop. It's a nop if the stream was already started.
|
||||
func (s *stream) Start() error {
|
||||
if s.ref != nilstream {
|
||||
return nil
|
||||
}
|
||||
wg.Wait()
|
||||
p := C.CFStringCreateWithCStringNoCopy(nil, C.CString(s.path), C.kCFStringEncodingUTF8, nil)
|
||||
path := C.CFArrayCreate(nil, (*unsafe.Pointer)(unsafe.Pointer(&p)), 1, nil)
|
||||
ctx := C.FSEventStreamContext{}
|
||||
ref := C.EventStreamCreate(&ctx, C.uintptr_t(s.info), path, C.FSEventStreamEventId(atomic.LoadUint64(&since)), latency, flags)
|
||||
if ref == nilstream {
|
||||
return errCreate
|
||||
}
|
||||
C.FSEventStreamScheduleWithRunLoop(ref, runloop, C.kCFRunLoopDefaultMode)
|
||||
if C.FSEventStreamStart(ref) == C.Boolean(0) {
|
||||
C.FSEventStreamInvalidate(ref)
|
||||
return errStart
|
||||
}
|
||||
C.CFRunLoopWakeUp(runloop)
|
||||
s.ref = ref
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops underlying FSEventStream and unregisters it from global runloop.
|
||||
func (s *stream) Stop() {
|
||||
if s.ref == nilstream {
|
||||
return
|
||||
}
|
||||
wg.Wait()
|
||||
C.FSEventStreamStop(s.ref)
|
||||
C.FSEventStreamInvalidate(s.ref)
|
||||
C.CFRunLoopWakeUp(runloop)
|
||||
s.ref = nilstream
|
||||
streamFuncs.delete(s.info)
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build darwin,!kqueue
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSplitflags(t *testing.T) {
|
||||
cases := [...]struct {
|
||||
set uint32
|
||||
flags []uint32
|
||||
}{
|
||||
{0, nil},
|
||||
{0xD, []uint32{0x1, 0x4, 0x8}},
|
||||
{0x0010 | 0x0040 | 0x0080 | 0x01000, []uint32{0x0010, 0x0040, 0x0080, 0x01000}},
|
||||
{0x40000 | 0x00100 | 0x00200, []uint32{0x00100, 0x00200, 0x40000}},
|
||||
}
|
||||
for i, cas := range cases {
|
||||
if flags := splitflags(cas.set); !reflect.DeepEqual(flags, cas.flags) {
|
||||
t.Errorf("want flags=%v; got %v (i=%d)", cas.flags, flags, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchStrip(t *testing.T) {
|
||||
const (
|
||||
create = uint32(FSEventsCreated)
|
||||
remove = uint32(FSEventsRemoved)
|
||||
rename = uint32(FSEventsRenamed)
|
||||
write = uint32(FSEventsModified)
|
||||
inode = uint32(FSEventsInodeMetaMod)
|
||||
owner = uint32(FSEventsChangeOwner)
|
||||
)
|
||||
cases := [...][]struct {
|
||||
path string
|
||||
flag uint32
|
||||
diff uint32
|
||||
}{
|
||||
// 1.
|
||||
{
|
||||
{"file", create | write, create | write},
|
||||
{"file", create | write | inode, write | inode},
|
||||
},
|
||||
// 2.
|
||||
{
|
||||
{"file", create, create},
|
||||
{"file", create | remove, remove},
|
||||
{"file", create | remove, create},
|
||||
},
|
||||
// 3.
|
||||
{
|
||||
{"file", create | write, create | write},
|
||||
{"file", create | write | owner, write | owner},
|
||||
},
|
||||
// 4.
|
||||
{
|
||||
{"file", create | write, create | write},
|
||||
{"file", write | inode, write | inode},
|
||||
{"file", remove | write | inode, remove},
|
||||
},
|
||||
{
|
||||
{"file", remove | write | inode, remove},
|
||||
},
|
||||
}
|
||||
Test:
|
||||
for i, cas := range cases {
|
||||
if len(cas) == 0 {
|
||||
t.Log("skipped")
|
||||
continue
|
||||
}
|
||||
w := &watch{prev: make(map[string]uint32)}
|
||||
for j, cas := range cas {
|
||||
if diff := w.strip(cas.path, cas.flag); diff != cas.diff {
|
||||
t.Errorf("want diff=%v; got %v (i=%d, j=%d)", Event(cas.diff),
|
||||
Event(diff), i, j)
|
||||
continue Test
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test for cases 3) and 5) with shadowed write&create events.
|
||||
//
|
||||
// See comment for (flagdiff).diff method.
|
||||
func TestWatcherShadowedWriteCreate(t *testing.T) {
|
||||
w := NewWatcherTest(t, "testdata/vfs.txt")
|
||||
defer w.Close()
|
||||
|
||||
cases := [...]WCase{
|
||||
// i=0
|
||||
create(w, "src/github.com/rjeczalik/fs/.fs.go.swp"),
|
||||
// i=1
|
||||
write(w, "src/github.com/rjeczalik/fs/.fs.go.swp", []byte("XD")),
|
||||
// i=2
|
||||
write(w, "src/github.com/rjeczalik/fs/.fs.go.swp", []byte("XD")),
|
||||
// i=3
|
||||
remove(w, "src/github.com/rjeczalik/fs/.fs.go.swp"),
|
||||
// i=4
|
||||
create(w, "src/github.com/rjeczalik/fs/.fs.go.swp"),
|
||||
// i=5
|
||||
write(w, "src/github.com/rjeczalik/fs/.fs.go.swp", []byte("XD")),
|
||||
}
|
||||
|
||||
w.ExpectAny(cases[:5]) // BUG(rjeczalik): #62
|
||||
}
|
||||
+405
@@ -0,0 +1,405 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build linux
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// eventBufferSize defines the size of the buffer given to read(2) function. One
|
||||
// should not depend on this value, since it was arbitrary chosen and may be
|
||||
// changed in the future.
|
||||
const eventBufferSize = 64 * (unix.SizeofInotifyEvent + unix.PathMax + 1)
|
||||
|
||||
// consumersCount defines the number of consumers in producer-consumer based
|
||||
// implementation. Each consumer is run in a separate goroutine and has read
|
||||
// access to watched files map.
|
||||
const consumersCount = 2
|
||||
|
||||
const invalidDescriptor = -1
|
||||
|
||||
// watched is a pair of file path and inotify mask used as a value in
|
||||
// watched files map.
|
||||
type watched struct {
|
||||
path string
|
||||
mask uint32
|
||||
}
|
||||
|
||||
// inotify implements Watcher interface.
|
||||
type inotify struct {
|
||||
sync.RWMutex // protects inotify.m map
|
||||
m map[int32]*watched // watch descriptor to watched object
|
||||
fd int32 // inotify file descriptor
|
||||
pipefd []int // pipe's read and write descriptors
|
||||
epfd int // epoll descriptor
|
||||
epes []unix.EpollEvent // epoll events
|
||||
buffer [eventBufferSize]byte // inotify event buffer
|
||||
wg sync.WaitGroup // wait group used to close main loop
|
||||
c chan<- EventInfo // event dispatcher channel
|
||||
}
|
||||
|
||||
// NewWatcher creates new non-recursive inotify backed by inotify.
|
||||
func newWatcher(c chan<- EventInfo) watcher {
|
||||
i := &inotify{
|
||||
m: make(map[int32]*watched),
|
||||
fd: invalidDescriptor,
|
||||
pipefd: []int{invalidDescriptor, invalidDescriptor},
|
||||
epfd: invalidDescriptor,
|
||||
epes: make([]unix.EpollEvent, 0),
|
||||
c: c,
|
||||
}
|
||||
runtime.SetFinalizer(i, func(i *inotify) {
|
||||
i.epollclose()
|
||||
if i.fd != invalidDescriptor {
|
||||
unix.Close(int(i.fd))
|
||||
}
|
||||
})
|
||||
return i
|
||||
}
|
||||
|
||||
// Watch implements notify.watcher interface.
|
||||
func (i *inotify) Watch(path string, e Event) error {
|
||||
return i.watch(path, e)
|
||||
}
|
||||
|
||||
// Rewatch implements notify.watcher interface.
|
||||
func (i *inotify) Rewatch(path string, _, newevent Event) error {
|
||||
return i.watch(path, newevent)
|
||||
}
|
||||
|
||||
// watch adds a new watcher to the set of watched objects or modifies the existing
|
||||
// one. If called for the first time, this function initializes inotify filesystem
|
||||
// monitor and starts producer-consumers goroutines.
|
||||
func (i *inotify) watch(path string, e Event) (err error) {
|
||||
if e&^(All|Event(unix.IN_ALL_EVENTS)) != 0 {
|
||||
return errors.New("notify: unknown event")
|
||||
}
|
||||
if err = i.lazyinit(); err != nil {
|
||||
return
|
||||
}
|
||||
iwd, err := unix.InotifyAddWatch(int(i.fd), path, encode(e))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
i.RLock()
|
||||
wd := i.m[int32(iwd)]
|
||||
i.RUnlock()
|
||||
if wd == nil {
|
||||
i.Lock()
|
||||
if i.m[int32(iwd)] == nil {
|
||||
i.m[int32(iwd)] = &watched{path: path, mask: uint32(e)}
|
||||
}
|
||||
i.Unlock()
|
||||
} else {
|
||||
i.Lock()
|
||||
wd.mask = uint32(e)
|
||||
i.Unlock()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// lazyinit sets up all required file descriptors and starts 1+consumersCount
|
||||
// goroutines. The producer goroutine blocks until file-system notifications
|
||||
// occur. Then, all events are read from system buffer and sent to consumer
|
||||
// goroutines which construct valid notify events. This method uses
|
||||
// Double-Checked Locking optimization.
|
||||
func (i *inotify) lazyinit() error {
|
||||
if atomic.LoadInt32(&i.fd) == invalidDescriptor {
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
if atomic.LoadInt32(&i.fd) == invalidDescriptor {
|
||||
fd, err := unix.InotifyInit1(unix.IN_CLOEXEC)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i.fd = int32(fd)
|
||||
if err = i.epollinit(); err != nil {
|
||||
_, _ = i.epollclose(), unix.Close(int(fd)) // Ignore errors.
|
||||
i.fd = invalidDescriptor
|
||||
return err
|
||||
}
|
||||
esch := make(chan []*event)
|
||||
go i.loop(esch)
|
||||
i.wg.Add(consumersCount)
|
||||
for n := 0; n < consumersCount; n++ {
|
||||
go i.send(esch)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// epollinit opens an epoll file descriptor and creates a pipe which will be
|
||||
// used to wake up the epoll_wait(2) function. Then, file descriptor associated
|
||||
// with inotify event queue and the read end of the pipe are added to epoll set.
|
||||
// Note that `fd` member must be set before this function is called.
|
||||
func (i *inotify) epollinit() (err error) {
|
||||
if i.epfd, err = unix.EpollCreate1(0); err != nil {
|
||||
return
|
||||
}
|
||||
if err = unix.Pipe(i.pipefd); err != nil {
|
||||
return
|
||||
}
|
||||
i.epes = []unix.EpollEvent{
|
||||
{Events: unix.EPOLLIN, Fd: i.fd},
|
||||
{Events: unix.EPOLLIN, Fd: int32(i.pipefd[0])},
|
||||
}
|
||||
if err = unix.EpollCtl(i.epfd, unix.EPOLL_CTL_ADD, int(i.fd), &i.epes[0]); err != nil {
|
||||
return
|
||||
}
|
||||
return unix.EpollCtl(i.epfd, unix.EPOLL_CTL_ADD, i.pipefd[0], &i.epes[1])
|
||||
}
|
||||
|
||||
// epollclose closes the file descriptor created by the call to epoll_create(2)
|
||||
// and two file descriptors opened by pipe(2) function.
|
||||
func (i *inotify) epollclose() (err error) {
|
||||
if i.epfd != invalidDescriptor {
|
||||
if err = unix.Close(i.epfd); err == nil {
|
||||
i.epfd = invalidDescriptor
|
||||
}
|
||||
}
|
||||
for n, fd := range i.pipefd {
|
||||
if fd != invalidDescriptor {
|
||||
switch e := unix.Close(fd); {
|
||||
case e != nil && err == nil:
|
||||
err = e
|
||||
case e == nil:
|
||||
i.pipefd[n] = invalidDescriptor
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// loop blocks until either inotify or pipe file descriptor is ready for I/O.
|
||||
// All read operations triggered by filesystem notifications are forwarded to
|
||||
// one of the event's consumers. If pipe fd became ready, loop function closes
|
||||
// all file descriptors opened by lazyinit method and returns afterwards.
|
||||
func (i *inotify) loop(esch chan<- []*event) {
|
||||
epes := make([]unix.EpollEvent, 1)
|
||||
fd := atomic.LoadInt32(&i.fd)
|
||||
for {
|
||||
switch _, err := unix.EpollWait(i.epfd, epes, -1); err {
|
||||
case nil:
|
||||
switch epes[0].Fd {
|
||||
case fd:
|
||||
esch <- i.read()
|
||||
epes[0].Fd = 0
|
||||
case int32(i.pipefd[0]):
|
||||
i.Lock()
|
||||
defer i.Unlock()
|
||||
if err = unix.Close(int(fd)); err != nil && err != unix.EINTR {
|
||||
panic("notify: close(2) error " + err.Error())
|
||||
}
|
||||
atomic.StoreInt32(&i.fd, invalidDescriptor)
|
||||
if err = i.epollclose(); err != nil && err != unix.EINTR {
|
||||
panic("notify: epollclose error " + err.Error())
|
||||
}
|
||||
close(esch)
|
||||
return
|
||||
}
|
||||
case unix.EINTR:
|
||||
continue
|
||||
default: // We should never reach this line.
|
||||
panic("notify: epoll_wait(2) error " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// read reads events from an inotify file descriptor. It does not handle errors
|
||||
// returned from read(2) function since they are not critical to watcher logic.
|
||||
func (i *inotify) read() (es []*event) {
|
||||
n, err := unix.Read(int(i.fd), i.buffer[:])
|
||||
if err != nil || n < unix.SizeofInotifyEvent {
|
||||
return
|
||||
}
|
||||
var sys *unix.InotifyEvent
|
||||
nmin := n - unix.SizeofInotifyEvent
|
||||
for pos, path := 0, ""; pos <= nmin; {
|
||||
sys = (*unix.InotifyEvent)(unsafe.Pointer(&i.buffer[pos]))
|
||||
pos += unix.SizeofInotifyEvent
|
||||
if path = ""; sys.Len > 0 {
|
||||
endpos := pos + int(sys.Len)
|
||||
path = string(bytes.TrimRight(i.buffer[pos:endpos], "\x00"))
|
||||
pos = endpos
|
||||
}
|
||||
es = append(es, &event{
|
||||
sys: unix.InotifyEvent{
|
||||
Wd: sys.Wd,
|
||||
Mask: sys.Mask,
|
||||
Cookie: sys.Cookie,
|
||||
},
|
||||
path: path,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// send is a consumer function which sends events to event dispatcher channel.
|
||||
// It is run in a separate goroutine in order to not block loop method when
|
||||
// possibly expensive write operations are performed on inotify map.
|
||||
func (i *inotify) send(esch <-chan []*event) {
|
||||
for es := range esch {
|
||||
for _, e := range i.transform(es) {
|
||||
if e != nil {
|
||||
i.c <- e
|
||||
}
|
||||
}
|
||||
}
|
||||
i.wg.Done()
|
||||
}
|
||||
|
||||
// transform prepares events read from inotify file descriptor for sending to
|
||||
// user. It removes invalid events and these which are no longer present in
|
||||
// inotify map. This method may also split one raw event into two different ones
|
||||
// when system-dependent result is required.
|
||||
func (i *inotify) transform(es []*event) []*event {
|
||||
var multi []*event
|
||||
i.RLock()
|
||||
for idx, e := range es {
|
||||
if e.sys.Mask&(unix.IN_IGNORED|unix.IN_Q_OVERFLOW) != 0 {
|
||||
es[idx] = nil
|
||||
continue
|
||||
}
|
||||
wd, ok := i.m[e.sys.Wd]
|
||||
if !ok || e.sys.Mask&encode(Event(wd.mask)) == 0 {
|
||||
es[idx] = nil
|
||||
continue
|
||||
}
|
||||
if e.path == "" {
|
||||
e.path = wd.path
|
||||
} else {
|
||||
e.path = filepath.Join(wd.path, e.path)
|
||||
}
|
||||
multi = append(multi, decode(Event(wd.mask), e))
|
||||
if e.event == 0 {
|
||||
es[idx] = nil
|
||||
}
|
||||
}
|
||||
i.RUnlock()
|
||||
es = append(es, multi...)
|
||||
return es
|
||||
}
|
||||
|
||||
// encode converts notify system-independent events to valid inotify mask
|
||||
// which can be passed to inotify_add_watch(2) function.
|
||||
func encode(e Event) uint32 {
|
||||
if e&Create != 0 {
|
||||
e = (e ^ Create) | InCreate | InMovedTo
|
||||
}
|
||||
if e&Remove != 0 {
|
||||
e = (e ^ Remove) | InDelete | InDeleteSelf
|
||||
}
|
||||
if e&Write != 0 {
|
||||
e = (e ^ Write) | InModify
|
||||
}
|
||||
if e&Rename != 0 {
|
||||
e = (e ^ Rename) | InMovedFrom | InMoveSelf
|
||||
}
|
||||
return uint32(e)
|
||||
}
|
||||
|
||||
// decode uses internally stored mask to distinguish whether system-independent
|
||||
// or system-dependent event is requested. The first one is created by modifying
|
||||
// `e` argument. decode method sets e.event value to 0 when an event should be
|
||||
// skipped. System-dependent event is set as the function's return value which
|
||||
// can be nil when the event should not be passed on.
|
||||
func decode(mask Event, e *event) (syse *event) {
|
||||
if sysmask := uint32(mask) & e.sys.Mask; sysmask != 0 {
|
||||
syse = &event{sys: unix.InotifyEvent{
|
||||
Wd: e.sys.Wd,
|
||||
Mask: e.sys.Mask,
|
||||
Cookie: e.sys.Cookie,
|
||||
}, event: Event(sysmask), path: e.path}
|
||||
}
|
||||
imask := encode(mask)
|
||||
switch {
|
||||
case mask&Create != 0 && imask&uint32(InCreate|InMovedTo)&e.sys.Mask != 0:
|
||||
e.event = Create
|
||||
case mask&Remove != 0 && imask&uint32(InDelete|InDeleteSelf)&e.sys.Mask != 0:
|
||||
e.event = Remove
|
||||
case mask&Write != 0 && imask&uint32(InModify)&e.sys.Mask != 0:
|
||||
e.event = Write
|
||||
case mask&Rename != 0 && imask&uint32(InMovedFrom|InMoveSelf)&e.sys.Mask != 0:
|
||||
e.event = Rename
|
||||
default:
|
||||
e.event = 0
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Unwatch implements notify.watcher interface. It looks for watch descriptor
|
||||
// related to registered path and if found, calls inotify_rm_watch(2) function.
|
||||
// This method is allowed to return EINVAL error when concurrently requested to
|
||||
// delete identical path.
|
||||
func (i *inotify) Unwatch(path string) (err error) {
|
||||
iwd := int32(invalidDescriptor)
|
||||
i.RLock()
|
||||
for iwdkey, wd := range i.m {
|
||||
if wd.path == path {
|
||||
iwd = iwdkey
|
||||
break
|
||||
}
|
||||
}
|
||||
i.RUnlock()
|
||||
if iwd == invalidDescriptor {
|
||||
return errors.New("notify: path " + path + " is already watched")
|
||||
}
|
||||
fd := atomic.LoadInt32(&i.fd)
|
||||
if err = removeInotifyWatch(fd, iwd); err != nil {
|
||||
return
|
||||
}
|
||||
i.Lock()
|
||||
delete(i.m, iwd)
|
||||
i.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close implements notify.watcher interface. It removes all existing watch
|
||||
// descriptors and wakes up producer goroutine by sending data to the write end
|
||||
// of the pipe. The function waits for a signal from producer which means that
|
||||
// all operations on current monitoring instance are done.
|
||||
func (i *inotify) Close() (err error) {
|
||||
i.Lock()
|
||||
if fd := atomic.LoadInt32(&i.fd); fd == invalidDescriptor {
|
||||
i.Unlock()
|
||||
return nil
|
||||
}
|
||||
for iwd := range i.m {
|
||||
if e := removeInotifyWatch(i.fd, iwd); e != nil && err == nil {
|
||||
err = e
|
||||
}
|
||||
delete(i.m, iwd)
|
||||
}
|
||||
switch _, errwrite := unix.Write(i.pipefd[1], []byte{0x00}); {
|
||||
case errwrite != nil && err == nil:
|
||||
err = errwrite
|
||||
fallthrough
|
||||
case errwrite != nil:
|
||||
i.Unlock()
|
||||
default:
|
||||
i.Unlock()
|
||||
i.wg.Wait()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// if path was removed, notify already removed the watch and returns EINVAL error
|
||||
func removeInotifyWatch(fd int32, iwd int32) (err error) {
|
||||
if _, err = unix.InotifyRmWatch(int(fd), uint32(iwd)); err != nil && err != unix.EINVAL {
|
||||
return
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build linux
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func icreate(w *W, path string) WCase {
|
||||
cas := create(w, path)
|
||||
cas.Events = append(cas.Events,
|
||||
&Call{P: path, E: InCreate},
|
||||
)
|
||||
return cas
|
||||
}
|
||||
|
||||
func iremove(w *W, path string) WCase {
|
||||
cas := remove(w, path)
|
||||
cas.Events = append(cas.Events,
|
||||
&Call{P: path, E: InDelete},
|
||||
)
|
||||
return cas
|
||||
}
|
||||
|
||||
func iopen(w *W, path string) WCase {
|
||||
return WCase{
|
||||
Action: func() {
|
||||
f, err := os.OpenFile(filepath.Join(w.root, path), os.O_RDWR, 0644)
|
||||
if err != nil {
|
||||
w.Fatalf("OpenFile(%q)=%v", path, err)
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
w.Fatalf("Close(%q)=%v", path, err)
|
||||
}
|
||||
},
|
||||
Events: []EventInfo{
|
||||
&Call{P: path, E: InAccess},
|
||||
&Call{P: path, E: InOpen},
|
||||
&Call{P: path, E: InCloseNowrite},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func iread(w *W, path string, p []byte) WCase {
|
||||
return WCase{
|
||||
Action: func() {
|
||||
f, err := os.OpenFile(filepath.Join(w.root, path), os.O_RDWR, 0644)
|
||||
if err != nil {
|
||||
w.Fatalf("OpenFile(%q)=%v", path, err)
|
||||
}
|
||||
if _, err := f.Read(p); err != nil {
|
||||
w.Fatalf("Read(%q)=%v", path, err)
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
w.Fatalf("Close(%q)=%v", path, err)
|
||||
}
|
||||
},
|
||||
Events: []EventInfo{
|
||||
&Call{P: path, E: InAccess},
|
||||
&Call{P: path, E: InOpen},
|
||||
&Call{P: path, E: InModify},
|
||||
&Call{P: path, E: InCloseNowrite},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func iwrite(w *W, path string, p []byte) WCase {
|
||||
cas := write(w, path, p)
|
||||
path = cas.Events[0].Path()
|
||||
cas.Events = append(cas.Events,
|
||||
&Call{P: path, E: InAccess},
|
||||
&Call{P: path, E: InOpen},
|
||||
&Call{P: path, E: InModify},
|
||||
&Call{P: path, E: InCloseWrite},
|
||||
)
|
||||
return cas
|
||||
}
|
||||
|
||||
func irename(w *W, path string) WCase {
|
||||
const ext = ".notify"
|
||||
return WCase{
|
||||
Action: func() {
|
||||
file := filepath.Join(w.root, path)
|
||||
if err := os.Rename(file, file+ext); err != nil {
|
||||
w.Fatalf("Rename(%q, %q)=%v", path, path+ext, err)
|
||||
}
|
||||
},
|
||||
Events: []EventInfo{
|
||||
&Call{P: path, E: InMovedFrom},
|
||||
&Call{P: path + ext, E: InMovedTo},
|
||||
&Call{P: path, E: InOpen},
|
||||
&Call{P: path, E: InAccess},
|
||||
&Call{P: path, E: InCreate},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var events = []Event{
|
||||
InAccess,
|
||||
InModify,
|
||||
InAttrib,
|
||||
InCloseWrite,
|
||||
InCloseNowrite,
|
||||
InOpen,
|
||||
InMovedFrom,
|
||||
InMovedTo,
|
||||
InCreate,
|
||||
InDelete,
|
||||
InDeleteSelf,
|
||||
InMoveSelf,
|
||||
}
|
||||
|
||||
func TestWatcherInotify(t *testing.T) {
|
||||
w := NewWatcherTest(t, "testdata/vfs.txt", events...)
|
||||
defer w.Close()
|
||||
|
||||
cases := [...]WCase{
|
||||
iopen(w, "src/github.com/rjeczalik/fs/fs.go"),
|
||||
iwrite(w, "src/github.com/rjeczalik/fs/fs.go", []byte("XD")),
|
||||
iread(w, "src/github.com/rjeczalik/fs/fs.go", []byte("XD")),
|
||||
iremove(w, "src/github.com/ppknap/link/README.md"),
|
||||
irename(w, "src/github.com/rjeczalik/fs/LICENSE"),
|
||||
}
|
||||
|
||||
w.ExpectAny(cases[:])
|
||||
}
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build darwin,kqueue dragonfly freebsd netbsd openbsd
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// newTrigger returns implementation of trigger.
|
||||
func newTrigger(pthLkp map[string]*watched) trigger {
|
||||
return &kq{
|
||||
pthLkp: pthLkp,
|
||||
idLkp: make(map[int]*watched),
|
||||
}
|
||||
}
|
||||
|
||||
// kq is a structure implementing trigger for kqueue.
|
||||
type kq struct {
|
||||
// fd is a kqueue file descriptor
|
||||
fd int
|
||||
// pipefds are file descriptors used to stop `Kevent` call.
|
||||
pipefds [2]int
|
||||
// idLkp is a data structure mapping file descriptors with data about watching
|
||||
// represented by them files/directories.
|
||||
idLkp map[int]*watched
|
||||
// pthLkp is a structure mapping monitored files/dir with data about them,
|
||||
// shared with parent trg structure
|
||||
pthLkp map[string]*watched
|
||||
}
|
||||
|
||||
// watched is a data structure representing watched file/directory.
|
||||
type watched struct {
|
||||
trgWatched
|
||||
// fd is a file descriptor for watched file/directory.
|
||||
fd int
|
||||
}
|
||||
|
||||
// Stop implements trigger.
|
||||
func (k *kq) Stop() (err error) {
|
||||
// trigger event used to interrupt Kevent call.
|
||||
_, err = syscall.Write(k.pipefds[1], []byte{0x00})
|
||||
return
|
||||
}
|
||||
|
||||
// Close implements trigger.
|
||||
func (k *kq) Close() error {
|
||||
return syscall.Close(k.fd)
|
||||
}
|
||||
|
||||
// NewWatched implements trigger.
|
||||
func (*kq) NewWatched(p string, fi os.FileInfo) (*watched, error) {
|
||||
fd, err := syscall.Open(p, syscall.O_NONBLOCK|syscall.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &watched{
|
||||
trgWatched: trgWatched{p: p, fi: fi},
|
||||
fd: fd,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Record implements trigger.
|
||||
func (k *kq) Record(w *watched) {
|
||||
k.idLkp[w.fd], k.pthLkp[w.p] = w, w
|
||||
}
|
||||
|
||||
// Del implements trigger.
|
||||
func (k *kq) Del(w *watched) {
|
||||
syscall.Close(w.fd)
|
||||
delete(k.idLkp, w.fd)
|
||||
delete(k.pthLkp, w.p)
|
||||
}
|
||||
|
||||
func inter2kq(n interface{}) syscall.Kevent_t {
|
||||
kq, ok := n.(syscall.Kevent_t)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("kqueue: type should be Kevent_t, %T instead", n))
|
||||
}
|
||||
return kq
|
||||
}
|
||||
|
||||
// Init implements trigger.
|
||||
func (k *kq) Init() (err error) {
|
||||
if k.fd, err = syscall.Kqueue(); err != nil {
|
||||
return
|
||||
}
|
||||
// Creates pipe used to stop `Kevent` call by registering it,
|
||||
// watching read end and writing to other end of it.
|
||||
if err = syscall.Pipe(k.pipefds[:]); err != nil {
|
||||
return nonil(err, k.Close())
|
||||
}
|
||||
var kevn [1]syscall.Kevent_t
|
||||
syscall.SetKevent(&kevn[0], k.pipefds[0], syscall.EVFILT_READ, syscall.EV_ADD)
|
||||
if _, err = syscall.Kevent(k.fd, kevn[:], nil, nil); err != nil {
|
||||
return nonil(err, k.Close())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Unwatch implements trigger.
|
||||
func (k *kq) Unwatch(w *watched) (err error) {
|
||||
var kevn [1]syscall.Kevent_t
|
||||
syscall.SetKevent(&kevn[0], w.fd, syscall.EVFILT_VNODE, syscall.EV_DELETE)
|
||||
|
||||
_, err = syscall.Kevent(k.fd, kevn[:], nil, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Watch implements trigger.
|
||||
func (k *kq) Watch(fi os.FileInfo, w *watched, e int64) (err error) {
|
||||
var kevn [1]syscall.Kevent_t
|
||||
syscall.SetKevent(&kevn[0], w.fd, syscall.EVFILT_VNODE,
|
||||
syscall.EV_ADD|syscall.EV_CLEAR)
|
||||
kevn[0].Fflags = uint32(e)
|
||||
|
||||
_, err = syscall.Kevent(k.fd, kevn[:], nil, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Wait implements trigger.
|
||||
func (k *kq) Wait() (interface{}, error) {
|
||||
var (
|
||||
kevn [1]syscall.Kevent_t
|
||||
err error
|
||||
)
|
||||
kevn[0] = syscall.Kevent_t{}
|
||||
_, err = syscall.Kevent(k.fd, nil, kevn[:], nil)
|
||||
|
||||
return kevn[0], err
|
||||
}
|
||||
|
||||
// Watched implements trigger.
|
||||
func (k *kq) Watched(n interface{}) (*watched, int64, error) {
|
||||
kevn, ok := n.(syscall.Kevent_t)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("kq: type should be syscall.Kevent_t, %T instead", kevn))
|
||||
}
|
||||
if _, ok = k.idLkp[int(kevn.Ident)]; !ok {
|
||||
return nil, 0, errNotWatched
|
||||
}
|
||||
return k.idLkp[int(kevn.Ident)], int64(kevn.Fflags), nil
|
||||
}
|
||||
|
||||
// IsStop implements trigger.
|
||||
func (k *kq) IsStop(n interface{}, err error) bool {
|
||||
return int(inter2kq(n).Ident) == k.pipefds[0]
|
||||
}
|
||||
|
||||
func init() {
|
||||
encode = func(e Event, dir bool) (o int64) {
|
||||
// Create event is not supported by kqueue. Instead NoteWrite event will
|
||||
// be registered for a directory. If this event will be reported on dir
|
||||
// which is to be monitored for Create, dir will be rescanned
|
||||
// and Create events will be generated and returned for new files.
|
||||
// In case of files, if not requested NoteRename event is reported,
|
||||
// it will be ignored.
|
||||
o = int64(e &^ Create)
|
||||
if (e&Create != 0 && dir) || e&Write != 0 {
|
||||
o = (o &^ int64(Write)) | int64(NoteWrite)
|
||||
}
|
||||
if e&Rename != 0 {
|
||||
o = (o &^ int64(Rename)) | int64(NoteRename)
|
||||
}
|
||||
if e&Remove != 0 {
|
||||
o = (o &^ int64(Remove)) | int64(NoteDelete)
|
||||
}
|
||||
return
|
||||
}
|
||||
nat2not = map[Event]Event{
|
||||
NoteWrite: Write,
|
||||
NoteRename: Rename,
|
||||
NoteDelete: Remove,
|
||||
NoteExtend: Event(0),
|
||||
NoteAttrib: Event(0),
|
||||
NoteRevoke: Event(0),
|
||||
NoteLink: Event(0),
|
||||
}
|
||||
not2nat = map[Event]Event{
|
||||
Write: NoteWrite,
|
||||
Rename: NoteRename,
|
||||
Remove: NoteDelete,
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build darwin,kqueue dragonfly freebsd netbsd openbsd
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func kqremove(w *W, path string, files []string) WCase {
|
||||
cas := remove(w, path)
|
||||
cas.Events[0] = &Call{P: path, E: NoteDelete}
|
||||
for _, f := range files {
|
||||
cas.Events = append(cas.Events, &Call{P: f, E: NoteDelete})
|
||||
}
|
||||
return cas
|
||||
}
|
||||
|
||||
func kqwrite(w *W, path string, p []byte) WCase {
|
||||
cas := write(w, path, p)
|
||||
path = cas.Events[0].Path()
|
||||
cas.Events[0] = &Call{P: path, E: NoteExtend | NoteWrite}
|
||||
return cas
|
||||
}
|
||||
|
||||
func kqrename(w *W, path string, files []string) WCase {
|
||||
const ext = ".notify"
|
||||
cas := WCase{
|
||||
Action: func() {
|
||||
file := filepath.Join(w.root, path)
|
||||
if err := os.Rename(file, file+ext); err != nil {
|
||||
w.Fatalf("Rename(%q, %q)=%v", path, path+ext, err)
|
||||
}
|
||||
},
|
||||
Events: []EventInfo{
|
||||
&Call{P: path + ext, E: osSpecificCreate},
|
||||
&Call{P: path, E: NoteRename},
|
||||
},
|
||||
}
|
||||
for _, f := range files {
|
||||
cas.Events = append(cas.Events, &Call{P: f, E: NoteRename})
|
||||
}
|
||||
return cas
|
||||
}
|
||||
|
||||
func kqlink(w *W, path string) WCase {
|
||||
const ext = ".notify"
|
||||
return WCase{
|
||||
Action: func() {
|
||||
file := filepath.Join(w.root, path)
|
||||
if err := os.Link(file, file+ext); err != nil {
|
||||
w.Fatalf("Link(%q, %q)=%v", path, path+ext, err)
|
||||
}
|
||||
},
|
||||
Events: []EventInfo{
|
||||
&Call{P: path, E: NoteLink},
|
||||
&Call{P: path + ext, E: osSpecificCreate},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var events = []Event{
|
||||
NoteWrite,
|
||||
NoteAttrib,
|
||||
NoteRename,
|
||||
osSpecificCreate,
|
||||
NoteDelete,
|
||||
NoteExtend,
|
||||
NoteLink,
|
||||
}
|
||||
|
||||
func TestWatcherKqueue(t *testing.T) {
|
||||
w := NewWatcherTest(t, "testdata/vfs.txt", events...)
|
||||
defer w.Close()
|
||||
|
||||
cases := [...]WCase{
|
||||
kqremove(w, "src/github.com/ppknap/link/include/coost/link", []string{
|
||||
"src/github.com/ppknap/link/include/coost/link/definitions.hpp",
|
||||
"src/github.com/ppknap/link/include/coost/link/detail/bundle.hpp",
|
||||
"src/github.com/ppknap/link/include/coost/link/detail/container_invoker.hpp",
|
||||
"src/github.com/ppknap/link/include/coost/link/detail/container_value_trait.hpp",
|
||||
"src/github.com/ppknap/link/include/coost/link/detail/dummy_type.hpp",
|
||||
"src/github.com/ppknap/link/include/coost/link/detail/function_trait.hpp",
|
||||
"src/github.com/ppknap/link/include/coost/link/detail/immediate_invoker.hpp",
|
||||
"src/github.com/ppknap/link/include/coost/link/detail/stdhelpers/always_same.hpp",
|
||||
"src/github.com/ppknap/link/include/coost/link/detail/stdhelpers/make_unique.hpp",
|
||||
"src/github.com/ppknap/link/include/coost/link/detail/stdhelpers",
|
||||
"src/github.com/ppknap/link/include/coost/link/detail/vertex.hpp",
|
||||
"src/github.com/ppknap/link/include/coost/link/detail/wire.hpp",
|
||||
"src/github.com/ppknap/link/include/coost/link/detail",
|
||||
"src/github.com/ppknap/link/include/coost/link/link.hpp",
|
||||
},
|
||||
),
|
||||
kqwrite(w, "src/github.com/rjeczalik/fs/fs.go", []byte("XD")),
|
||||
kqremove(w, "src/github.com/ppknap/link/README.md", nil),
|
||||
kqlink(w, "src/github.com/rjeczalik/fs/LICENSE"),
|
||||
kqrename(w, "src/github.com/rjeczalik/fs/fs.go", nil),
|
||||
kqrename(w, "src/github.com/rjeczalik/fs/cmd/gotree", []string{
|
||||
"src/github.com/rjeczalik/fs/cmd/gotree/go.go",
|
||||
"src/github.com/rjeczalik/fs/cmd/gotree/main.go",
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
w.ExpectAll(cases[:])
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) 2014-2018 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build !darwin,!linux,!freebsd,!dragonfly,!netbsd,!openbsd,!windows
|
||||
// +build !kqueue,!solaris
|
||||
|
||||
package notify
|
||||
|
||||
import "errors"
|
||||
|
||||
// newWatcher stub.
|
||||
func newWatcher(chan<- EventInfo) watcher {
|
||||
return watcherStub{errors.New("notify: not implemented")}
|
||||
}
|
||||
+605
@@ -0,0 +1,605 @@
|
||||
// Copyright (c) 2014-2018 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build windows
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// readBufferSize defines the size of an array in which read statuses are stored.
|
||||
// The buffer have to be DWORD-aligned and, if notify is used in monitoring a
|
||||
// directory over the network, its size must not be greater than 64KB. Each of
|
||||
// watched directories uses its own buffer for storing events.
|
||||
const readBufferSize = 4096
|
||||
|
||||
// Since all operations which go through the Windows completion routine are done
|
||||
// asynchronously, filter may set one of the constants below. They were defined
|
||||
// in order to distinguish whether current folder should be re-registered in
|
||||
// ReadDirectoryChangesW function or some control operations need to be executed.
|
||||
const (
|
||||
stateRewatch uint32 = 1 << (28 + iota)
|
||||
stateUnwatch
|
||||
stateCPClose
|
||||
)
|
||||
|
||||
// Filter used in current implementation was split into four segments:
|
||||
// - bits 0-11 store ReadDirectoryChangesW filters,
|
||||
// - bits 12-19 store File notify actions,
|
||||
// - bits 20-27 store notify specific events and flags,
|
||||
// - bits 28-31 store states which are used in loop's FSM.
|
||||
// Constants below are used as masks to retrieve only specific filter parts.
|
||||
const (
|
||||
onlyNotifyChanges uint32 = 0x00000FFF
|
||||
onlyNGlobalEvents uint32 = 0x0FF00000
|
||||
onlyMachineStates uint32 = 0xF0000000
|
||||
)
|
||||
|
||||
// grip represents a single watched directory. It stores the data required by
|
||||
// ReadDirectoryChangesW function. Only the filter, recursive, and handle members
|
||||
// may by modified by watcher implementation. Rest of the them have to remain
|
||||
// constant since they are used by Windows completion routine. This indicates that
|
||||
// grip can be removed only when all operations on the file handle are finished.
|
||||
type grip struct {
|
||||
handle syscall.Handle
|
||||
filter uint32
|
||||
recursive bool
|
||||
pathw []uint16
|
||||
buffer [readBufferSize]byte
|
||||
parent *watched
|
||||
ovlapped *overlappedEx
|
||||
}
|
||||
|
||||
// overlappedEx stores information used in asynchronous input and output.
|
||||
// Additionally, overlappedEx contains a pointer to 'grip' item which is used in
|
||||
// order to gather the structure in which the overlappedEx object was created.
|
||||
type overlappedEx struct {
|
||||
syscall.Overlapped
|
||||
parent *grip
|
||||
}
|
||||
|
||||
// newGrip creates a new file handle that can be used in overlapped operations.
|
||||
// Then, the handle is associated with I/O completion port 'cph' and its value
|
||||
// is stored in newly created 'grip' object.
|
||||
func newGrip(cph syscall.Handle, parent *watched, filter uint32) (*grip, error) {
|
||||
g := &grip{
|
||||
handle: syscall.InvalidHandle,
|
||||
filter: filter,
|
||||
recursive: parent.recursive,
|
||||
pathw: parent.pathw,
|
||||
parent: parent,
|
||||
ovlapped: &overlappedEx{},
|
||||
}
|
||||
if err := g.register(cph); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g.ovlapped.parent = g
|
||||
return g, nil
|
||||
}
|
||||
|
||||
// NOTE : Thread safe
|
||||
func (g *grip) register(cph syscall.Handle) (err error) {
|
||||
if g.handle, err = syscall.CreateFile(
|
||||
&g.pathw[0],
|
||||
syscall.FILE_LIST_DIRECTORY,
|
||||
syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE,
|
||||
nil,
|
||||
syscall.OPEN_EXISTING,
|
||||
syscall.FILE_FLAG_BACKUP_SEMANTICS|syscall.FILE_FLAG_OVERLAPPED,
|
||||
0,
|
||||
); err != nil {
|
||||
return
|
||||
}
|
||||
if _, err = syscall.CreateIoCompletionPort(g.handle, cph, 0, 0); err != nil {
|
||||
syscall.CloseHandle(g.handle)
|
||||
return
|
||||
}
|
||||
return g.readDirChanges()
|
||||
}
|
||||
|
||||
// readDirChanges tells the system to store file change information in grip's
|
||||
// buffer. Directory changes that occur between calls to this function are added
|
||||
// to the buffer and then, returned with the next call.
|
||||
func (g *grip) readDirChanges() error {
|
||||
handle := syscall.Handle(atomic.LoadUintptr((*uintptr)(&g.handle)))
|
||||
if handle == syscall.InvalidHandle {
|
||||
return nil // Handle was closed.
|
||||
}
|
||||
|
||||
return syscall.ReadDirectoryChanges(
|
||||
handle,
|
||||
&g.buffer[0],
|
||||
uint32(unsafe.Sizeof(g.buffer)),
|
||||
g.recursive,
|
||||
encode(g.filter),
|
||||
nil,
|
||||
(*syscall.Overlapped)(unsafe.Pointer(g.ovlapped)),
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
// encode transforms a generic filter, which contains platform independent and
|
||||
// implementation specific bit fields, to value that can be used as NotifyFilter
|
||||
// parameter in ReadDirectoryChangesW function.
|
||||
func encode(filter uint32) uint32 {
|
||||
e := Event(filter & (onlyNGlobalEvents | onlyNotifyChanges))
|
||||
if e&dirmarker != 0 {
|
||||
return uint32(FileNotifyChangeDirName)
|
||||
}
|
||||
if e&Create != 0 {
|
||||
e = (e ^ Create) | FileNotifyChangeFileName
|
||||
}
|
||||
if e&Remove != 0 {
|
||||
e = (e ^ Remove) | FileNotifyChangeFileName
|
||||
}
|
||||
if e&Write != 0 {
|
||||
e = (e ^ Write) | FileNotifyChangeAttributes | FileNotifyChangeSize |
|
||||
FileNotifyChangeCreation | FileNotifyChangeSecurity
|
||||
}
|
||||
if e&Rename != 0 {
|
||||
e = (e ^ Rename) | FileNotifyChangeFileName
|
||||
}
|
||||
return uint32(e)
|
||||
}
|
||||
|
||||
// watched is made in order to check whether an action comes from a directory or
|
||||
// file. This approach requires two file handlers per single monitored folder. The
|
||||
// second grip handles actions which include creating or deleting a directory. If
|
||||
// these processes are not monitored, only the first grip is created.
|
||||
type watched struct {
|
||||
filter uint32
|
||||
recursive bool
|
||||
count uint8
|
||||
pathw []uint16
|
||||
digrip [2]*grip
|
||||
}
|
||||
|
||||
// newWatched creates a new watched instance. It splits the filter variable into
|
||||
// two parts. The first part is responsible for watching all events which can be
|
||||
// created for a file in watched directory structure and the second one watches
|
||||
// only directory Create/Remove actions. If all operations succeed, the Create
|
||||
// message is sent to I/O completion port queue for further processing.
|
||||
func newWatched(cph syscall.Handle, filter uint32, recursive bool,
|
||||
path string) (wd *watched, err error) {
|
||||
wd = &watched{
|
||||
filter: filter,
|
||||
recursive: recursive,
|
||||
}
|
||||
if wd.pathw, err = syscall.UTF16FromString(path); err != nil {
|
||||
return
|
||||
}
|
||||
if err = wd.recreate(cph); err != nil {
|
||||
return
|
||||
}
|
||||
return wd, nil
|
||||
}
|
||||
|
||||
// TODO : doc
|
||||
func (wd *watched) recreate(cph syscall.Handle) (err error) {
|
||||
filefilter := wd.filter &^ uint32(FileNotifyChangeDirName)
|
||||
if err = wd.updateGrip(0, cph, filefilter == 0, filefilter); err != nil {
|
||||
return
|
||||
}
|
||||
dirfilter := wd.filter & uint32(FileNotifyChangeDirName|Create|Remove)
|
||||
if err = wd.updateGrip(1, cph, dirfilter == 0, wd.filter|uint32(dirmarker)); err != nil {
|
||||
return
|
||||
}
|
||||
wd.filter &^= onlyMachineStates
|
||||
return
|
||||
}
|
||||
|
||||
// TODO : doc
|
||||
func (wd *watched) updateGrip(idx int, cph syscall.Handle, reset bool,
|
||||
newflag uint32) (err error) {
|
||||
if reset {
|
||||
wd.digrip[idx] = nil
|
||||
} else {
|
||||
if wd.digrip[idx] == nil {
|
||||
if wd.digrip[idx], err = newGrip(cph, wd, newflag); err != nil {
|
||||
wd.closeHandle()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
wd.digrip[idx].filter = newflag
|
||||
wd.digrip[idx].recursive = wd.recursive
|
||||
if err = wd.digrip[idx].register(cph); err != nil {
|
||||
wd.closeHandle()
|
||||
return
|
||||
}
|
||||
}
|
||||
wd.count++
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// closeHandle closes handles that are stored in digrip array. Function always
|
||||
// tries to close all of the handlers before it exits, even when there are errors
|
||||
// returned from the operating system kernel.
|
||||
func (wd *watched) closeHandle() (err error) {
|
||||
for _, g := range wd.digrip {
|
||||
if g == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for {
|
||||
handle := syscall.Handle(atomic.LoadUintptr((*uintptr)(&g.handle)))
|
||||
if handle == syscall.InvalidHandle {
|
||||
break // Already closed.
|
||||
}
|
||||
|
||||
e := syscall.CloseHandle(handle)
|
||||
if e != nil && err == nil {
|
||||
err = e
|
||||
}
|
||||
|
||||
// Set invalid handle even when CloseHandle fails. This will leak
|
||||
// the handle but, since we can't close it anyway, there won't be
|
||||
// any difference.
|
||||
if atomic.CompareAndSwapUintptr((*uintptr)(&g.handle),
|
||||
(uintptr)(handle), (uintptr)(syscall.InvalidHandle)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// watcher implements Watcher interface. It stores a set of watched directories.
|
||||
// All operations which remove watched objects from map `m` must be performed in
|
||||
// loop goroutine since these structures are used internally by operating system.
|
||||
type readdcw struct {
|
||||
sync.Mutex
|
||||
m map[string]*watched
|
||||
cph syscall.Handle
|
||||
start bool
|
||||
wg sync.WaitGroup
|
||||
c chan<- EventInfo
|
||||
}
|
||||
|
||||
// NewWatcher creates new non-recursive watcher backed by ReadDirectoryChangesW.
|
||||
func newWatcher(c chan<- EventInfo) watcher {
|
||||
r := &readdcw{
|
||||
m: make(map[string]*watched),
|
||||
cph: syscall.InvalidHandle,
|
||||
c: c,
|
||||
}
|
||||
runtime.SetFinalizer(r, func(r *readdcw) {
|
||||
if r.cph != syscall.InvalidHandle {
|
||||
syscall.CloseHandle(r.cph)
|
||||
}
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
// Watch implements notify.Watcher interface.
|
||||
func (r *readdcw) Watch(path string, event Event) error {
|
||||
return r.watch(path, event, false)
|
||||
}
|
||||
|
||||
// RecursiveWatch implements notify.RecursiveWatcher interface.
|
||||
func (r *readdcw) RecursiveWatch(path string, event Event) error {
|
||||
return r.watch(path, event, true)
|
||||
}
|
||||
|
||||
// watch inserts a directory to the group of watched folders. If watched folder
|
||||
// already exists, function tries to rewatch it with new filters(NOT VALID). Moreover,
|
||||
// watch starts the main event loop goroutine when called for the first time.
|
||||
func (r *readdcw) watch(path string, event Event, recursive bool) error {
|
||||
if event&^(All|fileNotifyChangeAll) != 0 {
|
||||
return errors.New("notify: unknown event")
|
||||
}
|
||||
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
if wd, ok := r.m[path]; ok {
|
||||
dbgprint("watch: already exists")
|
||||
wd.filter &^= stateUnwatch
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := r.lazyinit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wd, err := newWatched(r.cph, uint32(event), recursive, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.m[path] = wd
|
||||
dbgprint("watch: new watch added")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// lazyinit creates an I/O completion port and starts the main event loop.
|
||||
func (r *readdcw) lazyinit() (err error) {
|
||||
invalid := uintptr(syscall.InvalidHandle)
|
||||
|
||||
if atomic.LoadUintptr((*uintptr)(&r.cph)) == invalid {
|
||||
cph := syscall.InvalidHandle
|
||||
if cph, err = syscall.CreateIoCompletionPort(cph, 0, 0, 0); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
r.cph, r.start = cph, true
|
||||
go r.loop()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(pknap) : doc
|
||||
func (r *readdcw) loop() {
|
||||
var n, key uint32
|
||||
var overlapped *syscall.Overlapped
|
||||
for {
|
||||
err := syscall.GetQueuedCompletionStatus(r.cph, &n, &key, &overlapped, syscall.INFINITE)
|
||||
if key == stateCPClose {
|
||||
r.Lock()
|
||||
handle := r.cph
|
||||
r.cph = syscall.InvalidHandle
|
||||
r.Unlock()
|
||||
syscall.CloseHandle(handle)
|
||||
r.wg.Done()
|
||||
return
|
||||
}
|
||||
if overlapped == nil {
|
||||
// TODO: check key == rewatch delete or 0(panic)
|
||||
continue
|
||||
}
|
||||
overEx := (*overlappedEx)(unsafe.Pointer(overlapped))
|
||||
if n != 0 {
|
||||
r.loopevent(n, overEx)
|
||||
if err = overEx.parent.readDirChanges(); err != nil {
|
||||
// TODO: error handling
|
||||
}
|
||||
}
|
||||
r.loopstate(overEx)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(pknap) : doc
|
||||
func (r *readdcw) loopstate(overEx *overlappedEx) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
filter := overEx.parent.parent.filter
|
||||
if filter&onlyMachineStates == 0 {
|
||||
return
|
||||
}
|
||||
if overEx.parent.parent.count--; overEx.parent.parent.count == 0 {
|
||||
switch filter & onlyMachineStates {
|
||||
case stateRewatch:
|
||||
dbgprint("loopstate rewatch")
|
||||
overEx.parent.parent.recreate(r.cph)
|
||||
case stateUnwatch:
|
||||
dbgprint("loopstate unwatch")
|
||||
overEx.parent.parent.closeHandle()
|
||||
delete(r.m, syscall.UTF16ToString(overEx.parent.pathw))
|
||||
case stateCPClose:
|
||||
default:
|
||||
panic(`notify: windows loopstate logic error`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(pknap) : doc
|
||||
func (r *readdcw) loopevent(n uint32, overEx *overlappedEx) {
|
||||
events := []*event{}
|
||||
var currOffset uint32
|
||||
for {
|
||||
raw := (*syscall.FileNotifyInformation)(unsafe.Pointer(&overEx.parent.buffer[currOffset]))
|
||||
name := syscall.UTF16ToString((*[syscall.MAX_LONG_PATH]uint16)(unsafe.Pointer(&raw.FileName))[:raw.FileNameLength>>1])
|
||||
events = append(events, &event{
|
||||
pathw: overEx.parent.pathw,
|
||||
filter: overEx.parent.filter,
|
||||
action: raw.Action,
|
||||
name: name,
|
||||
})
|
||||
if raw.NextEntryOffset == 0 {
|
||||
break
|
||||
}
|
||||
if currOffset += raw.NextEntryOffset; currOffset >= n {
|
||||
break
|
||||
}
|
||||
}
|
||||
r.send(events)
|
||||
}
|
||||
|
||||
// TODO(pknap) : doc
|
||||
func (r *readdcw) send(es []*event) {
|
||||
for _, e := range es {
|
||||
var syse Event
|
||||
if e.e, syse = decode(e.filter, e.action); e.e == 0 && syse == 0 {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case e.action == syscall.FILE_ACTION_MODIFIED:
|
||||
e.ftype = fTypeUnknown
|
||||
case e.filter&uint32(dirmarker) != 0:
|
||||
e.ftype = fTypeDirectory
|
||||
default:
|
||||
e.ftype = fTypeFile
|
||||
}
|
||||
switch {
|
||||
case e.e == 0:
|
||||
e.e = syse
|
||||
case syse != 0:
|
||||
r.c <- &event{
|
||||
pathw: e.pathw,
|
||||
name: e.name,
|
||||
ftype: e.ftype,
|
||||
action: e.action,
|
||||
filter: e.filter,
|
||||
e: syse,
|
||||
}
|
||||
}
|
||||
r.c <- e
|
||||
}
|
||||
}
|
||||
|
||||
// Rewatch implements notify.Rewatcher interface.
|
||||
func (r *readdcw) Rewatch(path string, oldevent, newevent Event) error {
|
||||
return r.rewatch(path, uint32(oldevent), uint32(newevent), false)
|
||||
}
|
||||
|
||||
// RecursiveRewatch implements notify.RecursiveRewatcher interface.
|
||||
func (r *readdcw) RecursiveRewatch(oldpath, newpath string, oldevent,
|
||||
newevent Event) error {
|
||||
if oldpath != newpath {
|
||||
if err := r.unwatch(oldpath); err != nil {
|
||||
return err
|
||||
}
|
||||
return r.watch(newpath, newevent, true)
|
||||
}
|
||||
return r.rewatch(newpath, uint32(oldevent), uint32(newevent), true)
|
||||
}
|
||||
|
||||
// TODO : (pknap) doc.
|
||||
func (r *readdcw) rewatch(path string, oldevent, newevent uint32, recursive bool) (err error) {
|
||||
if Event(newevent)&^(All|fileNotifyChangeAll) != 0 {
|
||||
return errors.New("notify: unknown event")
|
||||
}
|
||||
var wd *watched
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
if wd, err = r.nonStateWatchedLocked(path); err != nil {
|
||||
return
|
||||
}
|
||||
if wd.filter&(onlyNotifyChanges|onlyNGlobalEvents) != oldevent {
|
||||
panic(`notify: windows re-watcher logic error`)
|
||||
}
|
||||
wd.filter = stateRewatch | newevent
|
||||
wd.recursive, recursive = recursive, wd.recursive
|
||||
if err = wd.closeHandle(); err != nil {
|
||||
wd.filter = oldevent
|
||||
wd.recursive = recursive
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// TODO : pknap
|
||||
func (r *readdcw) nonStateWatchedLocked(path string) (wd *watched, err error) {
|
||||
wd, ok := r.m[path]
|
||||
if !ok || wd == nil {
|
||||
err = errors.New(`notify: ` + path + ` path is unwatched`)
|
||||
return
|
||||
}
|
||||
if wd.filter&onlyMachineStates != 0 {
|
||||
err = errors.New(`notify: another re/unwatching operation in progress`)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Unwatch implements notify.Watcher interface.
|
||||
func (r *readdcw) Unwatch(path string) error {
|
||||
return r.unwatch(path)
|
||||
}
|
||||
|
||||
// RecursiveUnwatch implements notify.RecursiveWatcher interface.
|
||||
func (r *readdcw) RecursiveUnwatch(path string) error {
|
||||
return r.unwatch(path)
|
||||
}
|
||||
|
||||
// TODO : pknap
|
||||
func (r *readdcw) unwatch(path string) (err error) {
|
||||
var wd *watched
|
||||
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
if wd, err = r.nonStateWatchedLocked(path); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
wd.filter |= stateUnwatch
|
||||
dbgprint("unwatch: set unwatch state")
|
||||
|
||||
if _, attrErr := syscall.GetFileAttributes(&wd.pathw[0]); attrErr != nil {
|
||||
for _, g := range wd.digrip {
|
||||
if g == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
dbgprint("unwatch: posting")
|
||||
if err = syscall.PostQueuedCompletionStatus(r.cph, 0, 0, (*syscall.Overlapped)(unsafe.Pointer(g.ovlapped))); err != nil {
|
||||
wd.filter &^= stateUnwatch
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Close resets the whole watcher object, closes all existing file descriptors,
|
||||
// and sends stateCPClose state as completion key to the main watcher's loop.
|
||||
func (r *readdcw) Close() (err error) {
|
||||
r.Lock()
|
||||
if !r.start {
|
||||
r.Unlock()
|
||||
return nil
|
||||
}
|
||||
for _, wd := range r.m {
|
||||
wd.filter &^= onlyMachineStates
|
||||
wd.filter |= stateCPClose
|
||||
if e := wd.closeHandle(); e != nil && err == nil {
|
||||
err = e
|
||||
}
|
||||
}
|
||||
r.start = false
|
||||
r.Unlock()
|
||||
r.wg.Add(1)
|
||||
if e := syscall.PostQueuedCompletionStatus(r.cph, 0, stateCPClose, nil); e != nil && err == nil {
|
||||
return e
|
||||
}
|
||||
r.wg.Wait()
|
||||
return
|
||||
}
|
||||
|
||||
// decode creates a notify event from both non-raw filter and action which was
|
||||
// returned from completion routine. Function may return Event(0) in case when
|
||||
// filter was replaced by a new value which does not contain fields that are
|
||||
// valid with passed action.
|
||||
func decode(filter, action uint32) (Event, Event) {
|
||||
switch action {
|
||||
case syscall.FILE_ACTION_ADDED:
|
||||
return gensys(filter, Create, FileActionAdded)
|
||||
case syscall.FILE_ACTION_REMOVED:
|
||||
return gensys(filter, Remove, FileActionRemoved)
|
||||
case syscall.FILE_ACTION_MODIFIED:
|
||||
return gensys(filter, Write, FileActionModified)
|
||||
case syscall.FILE_ACTION_RENAMED_OLD_NAME:
|
||||
return gensys(filter, Rename, FileActionRenamedOldName)
|
||||
case syscall.FILE_ACTION_RENAMED_NEW_NAME:
|
||||
return gensys(filter, Rename, FileActionRenamedNewName)
|
||||
}
|
||||
panic(`notify: cannot decode internal mask`)
|
||||
}
|
||||
|
||||
// gensys decides whether the Windows action, system-independent event or both
|
||||
// of them should be returned. Since the grip's filter may be atomically changed
|
||||
// during watcher lifetime, it is possible that neither Windows nor notify masks
|
||||
// are watched by the user when this function is called.
|
||||
func gensys(filter uint32, ge, se Event) (gene, syse Event) {
|
||||
isdir := filter&uint32(dirmarker) != 0
|
||||
if isdir && filter&uint32(FileNotifyChangeDirName) != 0 ||
|
||||
!isdir && filter&uint32(FileNotifyChangeFileName) != 0 ||
|
||||
filter&uint32(fileNotifyChangeModified) != 0 {
|
||||
syse = se
|
||||
}
|
||||
if filter&uint32(ge) != 0 {
|
||||
gene = ge
|
||||
}
|
||||
return
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build windows
|
||||
|
||||
package notify
|
||||
|
||||
import "testing"
|
||||
|
||||
// TODO(ppknap) : remove notify.Create event.
|
||||
func rcreate(w *W, path string) WCase {
|
||||
cas := create(w, path)
|
||||
cas.Events = append(cas.Events,
|
||||
&Call{P: path, E: FileActionAdded},
|
||||
)
|
||||
return cas
|
||||
}
|
||||
|
||||
// TODO(ppknap) : remove notify.Remove event.
|
||||
func rremove(w *W, path string) WCase {
|
||||
cas := remove(w, path)
|
||||
cas.Events = append(cas.Events,
|
||||
&Call{P: path, E: FileActionRemoved},
|
||||
)
|
||||
return cas
|
||||
}
|
||||
|
||||
// TODO(ppknap) : remove notify.Rename event.
|
||||
func rrename(w *W, oldpath, newpath string) WCase {
|
||||
cas := rename(w, oldpath, newpath)
|
||||
cas.Events = append(cas.Events,
|
||||
&Call{P: oldpath, E: FileActionRenamedOldName},
|
||||
&Call{P: newpath, E: FileActionRenamedNewName},
|
||||
)
|
||||
return cas
|
||||
}
|
||||
|
||||
// TODO(ppknap) : remove notify.Write event.
|
||||
func rwrite(w *W, path string, p []byte) WCase {
|
||||
cas := write(w, path, p)
|
||||
cas.Events = append(cas.Events,
|
||||
&Call{P: path, E: FileActionModified},
|
||||
)
|
||||
return cas
|
||||
}
|
||||
|
||||
var events = []Event{
|
||||
FileNotifyChangeFileName,
|
||||
FileNotifyChangeDirName,
|
||||
FileNotifyChangeSize,
|
||||
}
|
||||
|
||||
func TestWatcherReadDirectoryChangesW(t *testing.T) {
|
||||
w := NewWatcherTest(t, "testdata/vfs.txt", events...)
|
||||
defer w.Close()
|
||||
|
||||
cases := [...]WCase{
|
||||
rcreate(w, "src/github.com/rjeczalik/fs/fs_windows.go"),
|
||||
rcreate(w, "src/github.com/rjeczalik/fs/subdir/"),
|
||||
rremove(w, "src/github.com/rjeczalik/fs/fs.go"),
|
||||
rrename(w, "src/github.com/rjeczalik/fs/LICENSE", "src/github.com/rjeczalik/fs/COPYLEFT"),
|
||||
rwrite(w, "src/github.com/rjeczalik/fs/cmd/gotree/go.go", []byte("XD")),
|
||||
}
|
||||
|
||||
w.ExpectAny(cases[:])
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build darwin,!kqueue windows
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// noevent stripts test-case from expected event list, used when action is not
|
||||
// expected to trigger any events.
|
||||
func noevent(cas WCase) WCase {
|
||||
return WCase{Action: cas.Action}
|
||||
}
|
||||
|
||||
func TestWatcherRecursiveRewatch(t *testing.T) {
|
||||
w := newWatcherTest(t, "testdata/vfs.txt")
|
||||
defer w.Close()
|
||||
|
||||
cases := []WCase{
|
||||
create(w, "src/github.com/rjeczalik/file"),
|
||||
create(w, "src/github.com/rjeczalik/dir/"),
|
||||
noevent(create(w, "src/github.com/rjeczalik/fs/dir/")),
|
||||
noevent(create(w, "src/github.com/dir/")),
|
||||
noevent(write(w, "src/github.com/rjeczalik/file", []byte("XD"))),
|
||||
noevent(rename(w, "src/github.com/rjeczalik/fs/LICENSE", "src/LICENSE")),
|
||||
}
|
||||
|
||||
w.Watch("src/github.com/rjeczalik", Create)
|
||||
w.ExpectAny(cases)
|
||||
|
||||
cases = []WCase{
|
||||
create(w, "src/github.com/rjeczalik/fs/file"),
|
||||
create(w, "src/github.com/rjeczalik/fs/cmd/gotree/file"),
|
||||
create(w, "src/github.com/rjeczalik/fs/cmd/dir/"),
|
||||
create(w, "src/github.com/rjeczalik/fs/cmd/gotree/dir/"),
|
||||
noevent(write(w, "src/github.com/rjeczalik/fs/file", []byte("XD"))),
|
||||
noevent(create(w, "src/github.com/anotherdir/")),
|
||||
}
|
||||
|
||||
w.RecursiveRewatch("src/github.com/rjeczalik", "src/github.com/rjeczalik", Create, Create)
|
||||
w.ExpectAny(cases)
|
||||
|
||||
cases = []WCase{
|
||||
create(w, "src/github.com/rjeczalik/1"),
|
||||
create(w, "src/github.com/rjeczalik/2/"),
|
||||
noevent(create(w, "src/github.com/rjeczalik/fs/cmd/1")),
|
||||
noevent(create(w, "src/github.com/rjeczalik/fs/1/")),
|
||||
noevent(write(w, "src/github.com/rjeczalik/fs/file", []byte("XD"))),
|
||||
}
|
||||
|
||||
w.Rewatch("src/github.com/rjeczalik", Create, Create)
|
||||
w.ExpectAny(cases)
|
||||
}
|
||||
|
||||
// TODO(rjeczalik): move to watcher_test.go after #5
|
||||
func TestIsDirCreateEvent(t *testing.T) {
|
||||
w := NewWatcherTest(t, "testdata/vfs.txt")
|
||||
defer w.Close()
|
||||
|
||||
cases := [...]WCase{
|
||||
// i=0
|
||||
create(w, "src/github.com/jszwec/"),
|
||||
// i=1
|
||||
create(w, "src/github.com/jszwec/gojunitxml/"),
|
||||
// i=2
|
||||
create(w, "src/github.com/jszwec/gojunitxml/README.md"),
|
||||
// i=3
|
||||
create(w, "src/github.com/jszwec/gojunitxml/LICENSE"),
|
||||
// i=4
|
||||
create(w, "src/github.com/jszwec/gojunitxml/cmd/"),
|
||||
}
|
||||
|
||||
dirs := [...]bool{
|
||||
true, // i=0
|
||||
true, // i=1
|
||||
false, // i=2
|
||||
false, // i=3
|
||||
true, // i=4
|
||||
}
|
||||
|
||||
fn := func(i int, _ WCase, ei EventInfo) error {
|
||||
d, ok := ei.(isDirer)
|
||||
if !ok {
|
||||
return fmt.Errorf("received EventInfo does not implement isDirer")
|
||||
}
|
||||
switch ok, err := d.isDir(); {
|
||||
case err != nil:
|
||||
return err
|
||||
case ok != dirs[i]:
|
||||
return fmt.Errorf("want ok=%v; got %v", dirs[i], ok)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
w.ExpectAnyFunc(cases[:], fn)
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) 2014-2018 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package notify
|
||||
|
||||
type watcherStub struct{ error }
|
||||
|
||||
// Following methods implement notify.watcher interface.
|
||||
func (s watcherStub) Watch(string, Event) error { return s }
|
||||
func (s watcherStub) Rewatch(string, Event, Event) error { return s }
|
||||
func (s watcherStub) Unwatch(string) (err error) { return s }
|
||||
func (s watcherStub) Close() error { return s }
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build darwin linux freebsd dragonfly netbsd openbsd windows solaris
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NOTE Set NOTIFY_DEBUG env var or debug build tag for extra debugging info.
|
||||
|
||||
func TestWatcher(t *testing.T) {
|
||||
w := NewWatcherTest(t, "testdata/vfs.txt")
|
||||
defer w.Close()
|
||||
|
||||
cases := [...]WCase{
|
||||
create(w, "src/github.com/ppknap/link/include/coost/.link.hpp.swp"),
|
||||
create(w, "src/github.com/rjeczalik/fs/fs_test.go"),
|
||||
create(w, "src/github.com/rjeczalik/fs/binfs/"),
|
||||
create(w, "src/github.com/rjeczalik/fs/binfs.go"),
|
||||
create(w, "src/github.com/rjeczalik/fs/binfs_test.go"),
|
||||
remove(w, "src/github.com/rjeczalik/fs/binfs/"),
|
||||
create(w, "src/github.com/rjeczalik/fs/binfs/"),
|
||||
create(w, "src/github.com/rjeczalik/fs/virfs"),
|
||||
remove(w, "src/github.com/rjeczalik/fs/virfs"),
|
||||
create(w, "file"),
|
||||
create(w, "dir/"),
|
||||
}
|
||||
|
||||
w.ExpectAny(cases[:])
|
||||
}
|
||||
|
||||
// Simulates the scenario, where outside of the programs control the base dir
|
||||
// is removed. This is detected and the watch removed. Then the directory is
|
||||
// restored and a new watch set up.
|
||||
func TestStopPathNotExists(t *testing.T) {
|
||||
w := NewWatcherTest(t, "testdata/vfs.txt")
|
||||
defer w.Close()
|
||||
|
||||
if err := os.RemoveAll(w.root); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
Sync()
|
||||
|
||||
// Don't check the returned error, as the public function (notify.Stop)
|
||||
// does not return a potential error. As long as everything later on
|
||||
// works as inteded, that's fine
|
||||
time.Sleep(time.Duration(100) * time.Millisecond)
|
||||
w.Watcher.Unwatch(w.root)
|
||||
time.Sleep(time.Duration(100) * time.Millisecond)
|
||||
|
||||
if err := os.Mkdir(w.root, 0777); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
Sync()
|
||||
w.Watch("", All)
|
||||
|
||||
drainall(w.C)
|
||||
cases := [...]WCase{
|
||||
create(w, "file"),
|
||||
create(w, "dir/"),
|
||||
}
|
||||
w.ExpectAny(cases[:])
|
||||
}
|
||||
|
||||
func TestWatcherUnwatch(t *testing.T) {
|
||||
w := NewWatcherTest(t, "testdata/vfs.txt")
|
||||
defer w.Close()
|
||||
|
||||
remove(w, "src/github.com/ppknap/link/test/test_circular_calls.cpp").Action()
|
||||
w.Unwatch("")
|
||||
|
||||
w.Watch("", All)
|
||||
|
||||
drainall(w.C)
|
||||
cases := [...]WCase{
|
||||
create(w, "file"),
|
||||
create(w, "dir/"),
|
||||
}
|
||||
w.ExpectAny(cases[:])
|
||||
}
|
||||
+450
@@ -0,0 +1,450 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build darwin,kqueue dragonfly freebsd netbsd openbsd solaris
|
||||
|
||||
// watcher_trigger is used for FEN and kqueue which behave similarly:
|
||||
// only files and dirs can be watched directly, but not files inside dirs.
|
||||
// As a result Create events have to be generated by implementation when
|
||||
// after Write event is returned for watched dir, it is rescanned and Create
|
||||
// event is returned for new files and these are automatically added
|
||||
// to watchlist. In case of removal of watched directory, native system returns
|
||||
// events for all files, but for Rename, they also need to be generated.
|
||||
// As a result native system works as something like trigger for rescan,
|
||||
// but contains additional data about dir in which changes occurred. For files
|
||||
// detailed data is returned.
|
||||
// Usage of watcher_trigger requires:
|
||||
// - trigger implementation,
|
||||
// - encode func,
|
||||
// - not2nat, nat2not maps.
|
||||
// Required manual operations on filesystem can lead to loss of precision.
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// trigger is to be implemented by platform implementation like FEN or kqueue.
|
||||
type trigger interface {
|
||||
// Close closes watcher's main native file descriptor.
|
||||
Close() error
|
||||
// Stop waiting for new events.
|
||||
Stop() error
|
||||
// Create new instance of watched.
|
||||
NewWatched(string, os.FileInfo) (*watched, error)
|
||||
// Record internally new *watched instance.
|
||||
Record(*watched)
|
||||
// Del removes internal copy of *watched instance.
|
||||
Del(*watched)
|
||||
// Watched returns *watched instance and native events for native type.
|
||||
Watched(interface{}) (*watched, int64, error)
|
||||
// Init initializes native watcher call.
|
||||
Init() error
|
||||
// Watch starts watching provided file/dir.
|
||||
Watch(os.FileInfo, *watched, int64) error
|
||||
// Unwatch stops watching provided file/dir.
|
||||
Unwatch(*watched) error
|
||||
// Wait for new events.
|
||||
Wait() (interface{}, error)
|
||||
// IsStop checks if Wait finished because of request watcher's stop.
|
||||
IsStop(n interface{}, err error) bool
|
||||
}
|
||||
|
||||
// trgWatched is a the base data structure representing watched file/directory.
|
||||
// The platform specific full data structure (watched) must embed this type.
|
||||
type trgWatched struct {
|
||||
// p is a path to watched file/directory.
|
||||
p string
|
||||
// fi provides information about watched file/dir.
|
||||
fi os.FileInfo
|
||||
// eDir represents events watched directly.
|
||||
eDir Event
|
||||
// eNonDir represents events watched indirectly.
|
||||
eNonDir Event
|
||||
}
|
||||
|
||||
// encode Event to native representation. Implementation is to be provided by
|
||||
// platform specific implementation.
|
||||
var encode func(Event, bool) int64
|
||||
|
||||
var (
|
||||
// nat2not matches native events to notify's ones. To be initialized by
|
||||
// platform dependent implementation.
|
||||
nat2not map[Event]Event
|
||||
// not2nat matches notify's events to native ones. To be initialized by
|
||||
// platform dependent implementation.
|
||||
not2nat map[Event]Event
|
||||
)
|
||||
|
||||
// trg is a main structure implementing watcher.
|
||||
type trg struct {
|
||||
sync.Mutex
|
||||
// s is a channel used to stop monitoring.
|
||||
s chan struct{}
|
||||
// c is a channel used to pass events further.
|
||||
c chan<- EventInfo
|
||||
// pthLkp is a data structure mapping file names with data about watching
|
||||
// represented by them files/directories.
|
||||
pthLkp map[string]*watched
|
||||
// t is a platform dependent implementation of trigger.
|
||||
t trigger
|
||||
}
|
||||
|
||||
// newWatcher returns new watcher's implementation.
|
||||
func newWatcher(c chan<- EventInfo) watcher {
|
||||
t := &trg{
|
||||
s: make(chan struct{}, 1),
|
||||
pthLkp: make(map[string]*watched, 0),
|
||||
c: c,
|
||||
}
|
||||
t.t = newTrigger(t.pthLkp)
|
||||
if err := t.t.Init(); err != nil {
|
||||
t.Close()
|
||||
return watcherStub{fmt.Errorf("failed setting up watcher: %v", err)}
|
||||
}
|
||||
go t.monitor()
|
||||
return t
|
||||
}
|
||||
|
||||
// Close implements watcher.
|
||||
func (t *trg) Close() (err error) {
|
||||
t.Lock()
|
||||
if err = t.t.Stop(); err != nil {
|
||||
t.Unlock()
|
||||
return
|
||||
}
|
||||
<-t.s
|
||||
var e error
|
||||
for _, w := range t.pthLkp {
|
||||
if e = t.unwatch(w.p, w.fi); e != nil {
|
||||
dbgprintf("trg: unwatch %q failed: %q\n", w.p, e)
|
||||
err = nonil(err, e)
|
||||
}
|
||||
}
|
||||
if e = t.t.Close(); e != nil {
|
||||
dbgprintf("trg: closing native watch failed: %q\n", e)
|
||||
err = nonil(err, e)
|
||||
}
|
||||
if remaining := len(t.pthLkp); remaining != 0 {
|
||||
err = nonil(err, fmt.Errorf("Not all watches were removed: len(t.pthLkp) == %v", len(t.pthLkp)))
|
||||
}
|
||||
t.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// send reported events one by one through chan.
|
||||
func (t *trg) send(evn []event) {
|
||||
for i := range evn {
|
||||
t.c <- &evn[i]
|
||||
}
|
||||
}
|
||||
|
||||
// singlewatch starts to watch given p file/directory.
|
||||
func (t *trg) singlewatch(p string, e Event, direct mode, fi os.FileInfo) (err error) {
|
||||
w, ok := t.pthLkp[p]
|
||||
if !ok {
|
||||
if w, err = t.t.NewWatched(p, fi); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
switch direct {
|
||||
case dir:
|
||||
w.eDir |= e
|
||||
case ndir:
|
||||
w.eNonDir |= e
|
||||
case both:
|
||||
w.eDir |= e
|
||||
w.eNonDir |= e
|
||||
}
|
||||
if err = t.t.Watch(fi, w, encode(w.eDir|w.eNonDir, fi.IsDir())); err != nil {
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
t.t.Record(w)
|
||||
return nil
|
||||
}
|
||||
return errAlreadyWatched
|
||||
}
|
||||
|
||||
// decode converts event received from native to notify.Event
|
||||
// representation taking into account requested events (w).
|
||||
func decode(o int64, w Event) (e Event) {
|
||||
for f, n := range nat2not {
|
||||
if o&int64(f) != 0 {
|
||||
if w&f != 0 {
|
||||
e |= f
|
||||
}
|
||||
if w&n != 0 {
|
||||
e |= n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (t *trg) watch(p string, e Event, fi os.FileInfo) error {
|
||||
if err := t.singlewatch(p, e, dir, fi); err != nil {
|
||||
if err != errAlreadyWatched {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if fi.IsDir() {
|
||||
err := t.walk(p, func(fi os.FileInfo) (err error) {
|
||||
if err = t.singlewatch(filepath.Join(p, fi.Name()), e, ndir,
|
||||
fi); err != nil {
|
||||
if err != errAlreadyWatched {
|
||||
return
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// walk runs f func on each file/dir from p directory.
|
||||
func (t *trg) walk(p string, fn func(os.FileInfo) error) error {
|
||||
fp, err := os.Open(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ls, err := fp.Readdir(0)
|
||||
fp.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range ls {
|
||||
if err := fn(ls[i]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *trg) unwatch(p string, fi os.FileInfo) error {
|
||||
if fi.IsDir() {
|
||||
err := t.walk(p, func(fi os.FileInfo) error {
|
||||
err := t.singleunwatch(filepath.Join(p, fi.Name()), ndir)
|
||||
if err != errNotWatched {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return t.singleunwatch(p, dir)
|
||||
}
|
||||
|
||||
// Watch implements Watcher interface.
|
||||
func (t *trg) Watch(p string, e Event) error {
|
||||
fi, err := os.Stat(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.Lock()
|
||||
err = t.watch(p, e, fi)
|
||||
t.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
// Unwatch implements Watcher interface.
|
||||
func (t *trg) Unwatch(p string) error {
|
||||
fi, err := os.Stat(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.Lock()
|
||||
err = t.unwatch(p, fi)
|
||||
t.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
// Rewatch implements Watcher interface.
|
||||
//
|
||||
// TODO(rjeczalik): This is a naive hack. Rewrite might help.
|
||||
func (t *trg) Rewatch(p string, _, e Event) error {
|
||||
fi, err := os.Stat(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.Lock()
|
||||
if err = t.unwatch(p, fi); err == nil {
|
||||
// TODO(rjeczalik): If watch fails then we leave trigger in inconsistent
|
||||
// state. Handle? Panic? Native version of rewatch?
|
||||
err = t.watch(p, e, fi)
|
||||
}
|
||||
t.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*trg) file(w *watched, n interface{}, e Event) (evn []event) {
|
||||
evn = append(evn, event{w.p, e, w.fi.IsDir(), n})
|
||||
return
|
||||
}
|
||||
|
||||
func (t *trg) dir(w *watched, n interface{}, e, ge Event) (evn []event) {
|
||||
// If it's dir and delete we have to send it and continue, because
|
||||
// other processing relies on opening (in this case not existing) dir.
|
||||
// Events for contents of this dir are reported by native impl.
|
||||
// However events for rename must be generated for all monitored files
|
||||
// inside of moved directory, because native impl does not report it independently
|
||||
// for each file descriptor being moved in result of move action on
|
||||
// parent directory.
|
||||
if (ge & (not2nat[Rename] | not2nat[Remove])) != 0 {
|
||||
// Write is reported also for Remove on directory. Because of that
|
||||
// we have to filter it out explicitly.
|
||||
evn = append(evn, event{w.p, e & ^Write & ^not2nat[Write], true, n})
|
||||
if ge¬2nat[Rename] != 0 {
|
||||
for p := range t.pthLkp {
|
||||
if strings.HasPrefix(p, w.p+string(os.PathSeparator)) {
|
||||
if err := t.singleunwatch(p, both); err != nil && err != errNotWatched &&
|
||||
!os.IsNotExist(err) {
|
||||
dbgprintf("trg: failed stop watching moved file (%q): %q\n",
|
||||
p, err)
|
||||
}
|
||||
if (w.eDir|w.eNonDir)&(not2nat[Rename]|Rename) != 0 {
|
||||
evn = append(evn, event{
|
||||
p, (w.eDir | w.eNonDir) & e &^ Write &^ not2nat[Write],
|
||||
w.fi.IsDir(), nil,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
t.t.Del(w)
|
||||
return
|
||||
}
|
||||
if (ge & not2nat[Write]) != 0 {
|
||||
switch err := t.walk(w.p, func(fi os.FileInfo) error {
|
||||
p := filepath.Join(w.p, fi.Name())
|
||||
switch err := t.singlewatch(p, w.eDir, ndir, fi); {
|
||||
case os.IsNotExist(err) && ((w.eDir & Remove) != 0):
|
||||
evn = append(evn, event{p, Remove, fi.IsDir(), n})
|
||||
case err == errAlreadyWatched:
|
||||
case err != nil:
|
||||
dbgprintf("trg: watching %q failed: %q", p, err)
|
||||
case (w.eDir & Create) != 0:
|
||||
evn = append(evn, event{p, Create, fi.IsDir(), n})
|
||||
default:
|
||||
}
|
||||
return nil
|
||||
}); {
|
||||
case os.IsNotExist(err):
|
||||
return
|
||||
case err != nil:
|
||||
dbgprintf("trg: dir processing failed: %q", err)
|
||||
default:
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type mode uint
|
||||
|
||||
const (
|
||||
dir mode = iota
|
||||
ndir
|
||||
both
|
||||
)
|
||||
|
||||
// unwatch stops watching p file/directory.
|
||||
func (t *trg) singleunwatch(p string, direct mode) error {
|
||||
w, ok := t.pthLkp[p]
|
||||
if !ok {
|
||||
return errNotWatched
|
||||
}
|
||||
switch direct {
|
||||
case dir:
|
||||
w.eDir = 0
|
||||
case ndir:
|
||||
w.eNonDir = 0
|
||||
case both:
|
||||
w.eDir, w.eNonDir = 0, 0
|
||||
}
|
||||
if err := t.t.Unwatch(w); err != nil {
|
||||
return err
|
||||
}
|
||||
if w.eNonDir|w.eDir != 0 {
|
||||
mod := dir
|
||||
if w.eNonDir != 0 {
|
||||
mod = ndir
|
||||
}
|
||||
if err := t.singlewatch(p, w.eNonDir|w.eDir, mod,
|
||||
w.fi); err != nil && err != errAlreadyWatched {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
t.t.Del(w)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *trg) monitor() {
|
||||
var (
|
||||
n interface{}
|
||||
err error
|
||||
)
|
||||
for {
|
||||
switch n, err = t.t.Wait(); {
|
||||
case err == syscall.EINTR:
|
||||
case t.t.IsStop(n, err):
|
||||
t.s <- struct{}{}
|
||||
return
|
||||
case err != nil:
|
||||
dbgprintf("trg: failed to read events: %q\n", err)
|
||||
default:
|
||||
t.send(t.process(n))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// process event returned by native call.
|
||||
func (t *trg) process(n interface{}) (evn []event) {
|
||||
t.Lock()
|
||||
w, ge, err := t.t.Watched(n)
|
||||
if err != nil {
|
||||
t.Unlock()
|
||||
dbgprintf("trg: %v event lookup failed: %q", Event(ge), err)
|
||||
return
|
||||
}
|
||||
|
||||
e := decode(ge, w.eDir|w.eNonDir)
|
||||
if ge&int64(not2nat[Remove]|not2nat[Rename]) == 0 {
|
||||
switch fi, err := os.Stat(w.p); {
|
||||
case err != nil:
|
||||
default:
|
||||
if err = t.t.Watch(fi, w, encode(w.eDir|w.eNonDir, fi.IsDir())); err != nil {
|
||||
dbgprintf("trg: %q is no longer watched: %q", w.p, err)
|
||||
t.t.Del(w)
|
||||
}
|
||||
}
|
||||
}
|
||||
if e == Event(0) && (!w.fi.IsDir() || (ge&int64(not2nat[Write])) == 0) {
|
||||
t.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
if w.fi.IsDir() {
|
||||
evn = append(evn, t.dir(w, n, e, Event(ge))...)
|
||||
} else {
|
||||
evn = append(evn, t.file(w, n, e)...)
|
||||
}
|
||||
if Event(ge)&(not2nat[Remove]|not2nat[Rename]) != 0 {
|
||||
t.t.Del(w)
|
||||
}
|
||||
t.Unlock()
|
||||
return
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) 2014-2017 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build darwin,kqueue dragonfly freebsd netbsd openbsd solaris
|
||||
|
||||
package notify
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestWatcherCreateOnly(t *testing.T) {
|
||||
w := NewWatcherTest(t, "testdata/vfs.txt", Create)
|
||||
defer w.Close()
|
||||
|
||||
cases := [...]WCase{
|
||||
create(w, "dir/"),
|
||||
create(w, "dir2/"),
|
||||
}
|
||||
|
||||
w.ExpectAny(cases[:])
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package notify
|
||||
|
||||
// EventDiff describes a change to an event set - EventDiff[0] is an old state,
|
||||
// while EventDiff[1] is a new state. If event set has not changed (old == new),
|
||||
// functions typically return the None value.
|
||||
type eventDiff [2]Event
|
||||
|
||||
func (diff eventDiff) Event() Event {
|
||||
return diff[1] &^ diff[0]
|
||||
}
|
||||
|
||||
// Watchpoint
|
||||
//
|
||||
// The nil key holds total event set - logical sum for all registered events.
|
||||
// It speeds up computing EventDiff for Add method.
|
||||
//
|
||||
// The rec key holds an event set for a watchpoints created by RecursiveWatch
|
||||
// for a Watcher implementation which is not natively recursive.
|
||||
type watchpoint map[chan<- EventInfo]Event
|
||||
|
||||
// None is an empty event diff, think null object.
|
||||
var none eventDiff
|
||||
|
||||
// rec is just a placeholder
|
||||
var rec = func() (ch chan<- EventInfo) {
|
||||
ch = make(chan<- EventInfo)
|
||||
close(ch)
|
||||
return
|
||||
}()
|
||||
|
||||
func (wp watchpoint) dryAdd(ch chan<- EventInfo, e Event) eventDiff {
|
||||
if e &^= internal; wp[ch]&e == e {
|
||||
return none
|
||||
}
|
||||
total := wp[ch] &^ internal
|
||||
return eventDiff{total, total | e}
|
||||
}
|
||||
|
||||
// Add assumes neither c nor e are nil or zero values.
|
||||
func (wp watchpoint) Add(c chan<- EventInfo, e Event) (diff eventDiff) {
|
||||
wp[c] |= e
|
||||
diff[0] = wp[nil]
|
||||
diff[1] = diff[0] | e
|
||||
wp[nil] = diff[1] &^ omit
|
||||
// Strip diff from internal events.
|
||||
diff[0] &^= internal
|
||||
diff[1] &^= internal
|
||||
if diff[0] == diff[1] {
|
||||
return none
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (wp watchpoint) Del(c chan<- EventInfo, e Event) (diff eventDiff) {
|
||||
wp[c] &^= e
|
||||
if wp[c] == 0 {
|
||||
delete(wp, c)
|
||||
}
|
||||
diff[0] = wp[nil]
|
||||
delete(wp, nil)
|
||||
if len(wp) != 0 {
|
||||
// Recalculate total event set.
|
||||
for _, e := range wp {
|
||||
diff[1] |= e
|
||||
}
|
||||
wp[nil] = diff[1] &^ omit
|
||||
}
|
||||
// Strip diff from internal events.
|
||||
diff[0] &^= internal
|
||||
diff[1] &^= internal
|
||||
if diff[0] == diff[1] {
|
||||
return none
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (wp watchpoint) Dispatch(ei EventInfo, extra Event) {
|
||||
e := eventmask(ei, extra)
|
||||
if !matches(wp[nil], e) {
|
||||
return
|
||||
}
|
||||
for ch, eset := range wp {
|
||||
if ch != nil && matches(eset, e) {
|
||||
select {
|
||||
case ch <- ei:
|
||||
default: // Drop event if receiver is too slow
|
||||
dbgprintf("dropped %s on %q: receiver too slow", ei.Event(), ei.Path())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (wp watchpoint) Total() Event {
|
||||
return wp[nil] &^ internal
|
||||
}
|
||||
|
||||
func (wp watchpoint) IsRecursive() bool {
|
||||
return wp[nil]&recursive != 0
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build !windows
|
||||
|
||||
package notify
|
||||
|
||||
// eventmask uses ei to create a new event which contains internal flags used by
|
||||
// notify package logic.
|
||||
func eventmask(ei EventInfo, extra Event) Event {
|
||||
return ei.Event() | extra
|
||||
}
|
||||
|
||||
// matches reports a match only when:
|
||||
//
|
||||
// - for user events, when event is present in the given set
|
||||
// - for internal events, when additionally both event and set have omit bit set
|
||||
//
|
||||
// Internal events must not be sent to user channels and vice versa.
|
||||
func matches(set, event Event) bool {
|
||||
return (set&omit)^(event&omit) == 0 && set&event == event
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
// +build windows
|
||||
|
||||
package notify
|
||||
|
||||
// eventmask uses ei to create a new event which contains internal flags used by
|
||||
// notify package logic. If one of FileAction* masks is detected, this function
|
||||
// adds corresponding FileNotifyChange* values. This allows non registered
|
||||
// FileAction* events to be passed on.
|
||||
func eventmask(ei EventInfo, extra Event) (e Event) {
|
||||
if e = ei.Event() | extra; e&fileActionAll != 0 {
|
||||
if ev, ok := ei.(*event); ok {
|
||||
switch ev.ftype {
|
||||
case fTypeFile:
|
||||
e |= FileNotifyChangeFileName
|
||||
case fTypeDirectory:
|
||||
e |= FileNotifyChangeDirName
|
||||
case fTypeUnknown:
|
||||
e |= fileNotifyChangeModified
|
||||
}
|
||||
return e &^ fileActionAll
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// matches reports a match only when:
|
||||
//
|
||||
// - for user events, when event is present in the given set
|
||||
// - for internal events, when additionally both event and set have omit bit set
|
||||
//
|
||||
// Internal events must not be sent to user channels and vice versa.
|
||||
func matches(set, event Event) bool {
|
||||
return (set&omit)^(event&omit) == 0 && (set&event == event || set&fileNotifyChangeModified&event != 0)
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
// Copyright (c) 2014-2015 The Notify Authors. All rights reserved.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package notify
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func call(wp watchpoint, fn interface{}, args []interface{}) eventDiff {
|
||||
vals := []reflect.Value{reflect.ValueOf(wp)}
|
||||
for _, arg := range args {
|
||||
vals = append(vals, reflect.ValueOf(arg))
|
||||
}
|
||||
res := reflect.ValueOf(fn).Call(vals)
|
||||
if n := len(res); n != 1 {
|
||||
panic(fmt.Sprintf("unexpected len(res)=%d", n))
|
||||
}
|
||||
diff, ok := res[0].Interface().(eventDiff)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("want typeof(diff)=EventDiff; got %T", res[0].Interface()))
|
||||
}
|
||||
return diff
|
||||
}
|
||||
|
||||
func TestWatchpoint(t *testing.T) {
|
||||
ch := NewChans(5)
|
||||
all := All | recursive
|
||||
cases := [...]struct {
|
||||
fn interface{}
|
||||
args []interface{}
|
||||
diff eventDiff
|
||||
total Event
|
||||
}{
|
||||
// i=0
|
||||
{
|
||||
watchpoint.Add,
|
||||
[]interface{}{ch[0], Remove},
|
||||
eventDiff{0, Remove},
|
||||
Remove,
|
||||
},
|
||||
// i=1
|
||||
{
|
||||
watchpoint.Add,
|
||||
[]interface{}{ch[1], Create | Remove | recursive},
|
||||
eventDiff{Remove, Remove | Create},
|
||||
Create | Remove | recursive,
|
||||
},
|
||||
// i=2
|
||||
{
|
||||
watchpoint.Add,
|
||||
[]interface{}{ch[2], Create | Rename},
|
||||
eventDiff{Create | Remove, Create | Remove | Rename},
|
||||
Create | Remove | Rename | recursive,
|
||||
},
|
||||
// i=3
|
||||
{
|
||||
watchpoint.Add,
|
||||
[]interface{}{ch[0], Write | recursive},
|
||||
eventDiff{Create | Remove | Rename, Create | Remove | Rename | Write},
|
||||
Create | Remove | Rename | Write | recursive,
|
||||
},
|
||||
// i=4
|
||||
{
|
||||
watchpoint.Add,
|
||||
[]interface{}{ch[2], Remove | recursive},
|
||||
none,
|
||||
Create | Remove | Rename | Write | recursive,
|
||||
},
|
||||
// i=5
|
||||
{
|
||||
watchpoint.Del,
|
||||
[]interface{}{ch[0], all},
|
||||
eventDiff{Create | Remove | Rename | Write, Create | Remove | Rename},
|
||||
Create | Remove | Rename | recursive,
|
||||
},
|
||||
// i=6
|
||||
{
|
||||
watchpoint.Del,
|
||||
[]interface{}{ch[2], all},
|
||||
eventDiff{Create | Remove | Rename, Create | Remove},
|
||||
Create | Remove | recursive,
|
||||
},
|
||||
// i=7
|
||||
{
|
||||
watchpoint.Add,
|
||||
[]interface{}{ch[3], Create | Remove},
|
||||
none,
|
||||
Create | Remove | recursive,
|
||||
},
|
||||
// i=8
|
||||
{
|
||||
watchpoint.Del,
|
||||
[]interface{}{ch[1], all},
|
||||
none,
|
||||
Create | Remove,
|
||||
},
|
||||
// i=9
|
||||
{
|
||||
watchpoint.Add,
|
||||
[]interface{}{ch[3], recursive | Write},
|
||||
eventDiff{Create | Remove, Create | Remove | Write},
|
||||
Create | Remove | Write | recursive,
|
||||
},
|
||||
// i=10
|
||||
{
|
||||
watchpoint.Del,
|
||||
[]interface{}{ch[3], Create},
|
||||
eventDiff{Create | Remove | Write, Remove | Write},
|
||||
Remove | Write | recursive,
|
||||
},
|
||||
// i=11
|
||||
{
|
||||
watchpoint.Add,
|
||||
[]interface{}{ch[3], Create | Rename},
|
||||
eventDiff{Remove | Write, Create | Remove | Rename | Write},
|
||||
Create | Remove | Rename | Write | recursive,
|
||||
},
|
||||
// i=12
|
||||
{
|
||||
watchpoint.Add,
|
||||
[]interface{}{ch[2], Remove | Write},
|
||||
none,
|
||||
Create | Remove | Rename | Write | recursive,
|
||||
},
|
||||
// i=13
|
||||
{
|
||||
watchpoint.Del,
|
||||
[]interface{}{ch[3], Create | Remove | Write},
|
||||
eventDiff{Create | Remove | Rename | Write, Remove | Rename | Write},
|
||||
Remove | Rename | Write | recursive,
|
||||
},
|
||||
// i=14
|
||||
{
|
||||
watchpoint.Del,
|
||||
[]interface{}{ch[2], Remove},
|
||||
eventDiff{Remove | Rename | Write, Rename | Write},
|
||||
Rename | Write | recursive,
|
||||
},
|
||||
// i=15
|
||||
{
|
||||
watchpoint.Del,
|
||||
[]interface{}{ch[3], Rename | recursive},
|
||||
eventDiff{Rename | Write, Write},
|
||||
Write,
|
||||
},
|
||||
}
|
||||
wp := watchpoint{}
|
||||
for i, cas := range cases {
|
||||
if diff := call(wp, cas.fn, cas.args); diff != cas.diff {
|
||||
t.Errorf("want diff=%v; got %v (i=%d)", cas.diff, diff, i)
|
||||
continue
|
||||
}
|
||||
if total := wp[nil]; total != cas.total {
|
||||
t.Errorf("want total=%v; got %v (i=%d)", cas.total, total, i)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+10
@@ -0,0 +1,10 @@
|
||||
# Treat all files in this repo as binary, with no git magic updating
|
||||
# line endings. Windows users contributing to Go will need to use a
|
||||
# modern version of git and editors capable of LF line endings.
|
||||
#
|
||||
# We'll prevent accidental CRLF line endings from entering the repo
|
||||
# via the git-review gofmt checks.
|
||||
#
|
||||
# See golang.org/issue/9281
|
||||
|
||||
* -text
|
||||
Generated
+2
@@ -0,0 +1,2 @@
|
||||
# Add no patterns to .gitignore except for files generated by the build.
|
||||
last-change
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
# This source code refers to The Go Authors for copyright purposes.
|
||||
# The master list of authors is in the main Go distribution,
|
||||
# visible at http://tip.golang.org/AUTHORS.
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
# Contributing to Go
|
||||
|
||||
Go is an open source project.
|
||||
|
||||
It is the work of hundreds of contributors. We appreciate your help!
|
||||
|
||||
## Filing issues
|
||||
|
||||
When [filing an issue](https://golang.org/issue/new), make sure to answer these five questions:
|
||||
|
||||
1. What version of Go are you using (`go version`)?
|
||||
2. What operating system and processor architecture are you using?
|
||||
3. What did you do?
|
||||
4. What did you expect to see?
|
||||
5. What did you see instead?
|
||||
|
||||
General questions should go to the [golang-nuts mailing list](https://groups.google.com/group/golang-nuts) instead of the issue tracker.
|
||||
The gophers there will answer or ask you to file an issue if you've tripped over a bug.
|
||||
|
||||
## Contributing code
|
||||
|
||||
Please read the [Contribution Guidelines](https://golang.org/doc/contribute.html)
|
||||
before sending patches.
|
||||
|
||||
Unless otherwise noted, the Go source files are distributed under
|
||||
the BSD-style license found in the LICENSE file.
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
# This source code was written by the Go contributors.
|
||||
# The master list of contributors is in the main Go distribution,
|
||||
# visible at http://tip.golang.org/CONTRIBUTORS.
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
Additional IP Rights Grant (Patents)
|
||||
|
||||
"This implementation" means the copyrightable works distributed by
|
||||
Google as part of the Go project.
|
||||
|
||||
Google hereby grants to You a perpetual, worldwide, non-exclusive,
|
||||
no-charge, royalty-free, irrevocable (except as stated in this section)
|
||||
patent license to make, have made, use, offer to sell, sell, import,
|
||||
transfer and otherwise run, modify and propagate the contents of this
|
||||
implementation of Go, where such license applies only to those patent
|
||||
claims, both currently owned or controlled by Google and acquired in
|
||||
the future, licensable by Google that are necessarily infringed by this
|
||||
implementation of Go. This grant does not include claims that would be
|
||||
infringed only as a consequence of further modification of this
|
||||
implementation. If you or your agent or exclusive licensee institute or
|
||||
order or agree to the institution of patent litigation against any
|
||||
entity (including a cross-claim or counterclaim in a lawsuit) alleging
|
||||
that this implementation of Go or any code incorporated within this
|
||||
implementation of Go constitutes direct or contributory patent
|
||||
infringement, or inducement of patent infringement, then any patent
|
||||
rights granted to you under this License for this implementation of Go
|
||||
shall terminate as of the date such litigation is filed.
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
# Go Tools
|
||||
|
||||
This subrepository holds the source for various packages and tools that support
|
||||
the Go programming language.
|
||||
|
||||
Some of the tools, `godoc` and `vet` for example, are included in binary Go
|
||||
distributions.
|
||||
|
||||
Others, including the Go `guru` and the test coverage tool, can be fetched with
|
||||
`go get`.
|
||||
|
||||
Packages include a type-checker for Go and an implementation of the
|
||||
Static Single Assignment form (SSA) representation for Go programs.
|
||||
|
||||
## Download/Install
|
||||
|
||||
The easiest way to install is to run `go get -u golang.org/x/tools/...`. You can
|
||||
also manually git clone the repository to `$GOPATH/src/golang.org/x/tools`.
|
||||
|
||||
## Report Issues / Send Patches
|
||||
|
||||
This repository uses Gerrit for code changes. To learn how to submit changes to
|
||||
this repository, see https://golang.org/doc/contribute.html.
|
||||
|
||||
The main issue tracker for the tools repository is located at
|
||||
https://github.com/golang/go/issues. Prefix your issue with "x/tools/(your
|
||||
subdir):" in the subject line, so it is easy to find.
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package parse provides support for parsing benchmark results as
|
||||
// generated by 'go test -bench'.
|
||||
package parse // import "golang.org/x/tools/benchmark/parse"
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Flags used by Benchmark.Measured to indicate
|
||||
// which measurements a Benchmark contains.
|
||||
const (
|
||||
NsPerOp = 1 << iota
|
||||
MBPerS
|
||||
AllocedBytesPerOp
|
||||
AllocsPerOp
|
||||
)
|
||||
|
||||
// Benchmark is one run of a single benchmark.
|
||||
type Benchmark struct {
|
||||
Name string // benchmark name
|
||||
N int // number of iterations
|
||||
NsPerOp float64 // nanoseconds per iteration
|
||||
AllocedBytesPerOp uint64 // bytes allocated per iteration
|
||||
AllocsPerOp uint64 // allocs per iteration
|
||||
MBPerS float64 // MB processed per second
|
||||
Measured int // which measurements were recorded
|
||||
Ord int // ordinal position within a benchmark run
|
||||
}
|
||||
|
||||
// ParseLine extracts a Benchmark from a single line of testing.B
|
||||
// output.
|
||||
func ParseLine(line string) (*Benchmark, error) {
|
||||
fields := strings.Fields(line)
|
||||
|
||||
// Two required, positional fields: Name and iterations.
|
||||
if len(fields) < 2 {
|
||||
return nil, fmt.Errorf("two fields required, have %d", len(fields))
|
||||
}
|
||||
if !strings.HasPrefix(fields[0], "Benchmark") {
|
||||
return nil, fmt.Errorf(`first field does not start with "Benchmark"`)
|
||||
}
|
||||
n, err := strconv.Atoi(fields[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b := &Benchmark{Name: fields[0], N: n}
|
||||
|
||||
// Parse any remaining pairs of fields; we've parsed one pair already.
|
||||
for i := 1; i < len(fields)/2; i++ {
|
||||
b.parseMeasurement(fields[i*2], fields[i*2+1])
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (b *Benchmark) parseMeasurement(quant string, unit string) {
|
||||
switch unit {
|
||||
case "ns/op":
|
||||
if f, err := strconv.ParseFloat(quant, 64); err == nil {
|
||||
b.NsPerOp = f
|
||||
b.Measured |= NsPerOp
|
||||
}
|
||||
case "MB/s":
|
||||
if f, err := strconv.ParseFloat(quant, 64); err == nil {
|
||||
b.MBPerS = f
|
||||
b.Measured |= MBPerS
|
||||
}
|
||||
case "B/op":
|
||||
if i, err := strconv.ParseUint(quant, 10, 64); err == nil {
|
||||
b.AllocedBytesPerOp = i
|
||||
b.Measured |= AllocedBytesPerOp
|
||||
}
|
||||
case "allocs/op":
|
||||
if i, err := strconv.ParseUint(quant, 10, 64); err == nil {
|
||||
b.AllocsPerOp = i
|
||||
b.Measured |= AllocsPerOp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Benchmark) String() string {
|
||||
buf := new(bytes.Buffer)
|
||||
fmt.Fprintf(buf, "%s %d", b.Name, b.N)
|
||||
if (b.Measured & NsPerOp) != 0 {
|
||||
fmt.Fprintf(buf, " %.2f ns/op", b.NsPerOp)
|
||||
}
|
||||
if (b.Measured & MBPerS) != 0 {
|
||||
fmt.Fprintf(buf, " %.2f MB/s", b.MBPerS)
|
||||
}
|
||||
if (b.Measured & AllocedBytesPerOp) != 0 {
|
||||
fmt.Fprintf(buf, " %d B/op", b.AllocedBytesPerOp)
|
||||
}
|
||||
if (b.Measured & AllocsPerOp) != 0 {
|
||||
fmt.Fprintf(buf, " %d allocs/op", b.AllocsPerOp)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// Set is a collection of benchmarks from one
|
||||
// testing.B run, keyed by name to facilitate comparison.
|
||||
type Set map[string][]*Benchmark
|
||||
|
||||
// ParseSet extracts a Set from testing.B output.
|
||||
// ParseSet preserves the order of benchmarks that have identical
|
||||
// names.
|
||||
func ParseSet(r io.Reader) (Set, error) {
|
||||
bb := make(Set)
|
||||
scan := bufio.NewScanner(r)
|
||||
ord := 0
|
||||
for scan.Scan() {
|
||||
if b, err := ParseLine(scan.Text()); err == nil {
|
||||
b.Ord = ord
|
||||
ord++
|
||||
bb[b.Name] = append(bb[b.Name], b)
|
||||
}
|
||||
}
|
||||
|
||||
if err := scan.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bb, nil
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package parse
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseLine(t *testing.T) {
|
||||
cases := []struct {
|
||||
line string
|
||||
want *Benchmark
|
||||
err bool // expect an error
|
||||
}{
|
||||
{
|
||||
line: "BenchmarkEncrypt 100000000 19.6 ns/op",
|
||||
want: &Benchmark{
|
||||
Name: "BenchmarkEncrypt",
|
||||
N: 100000000, NsPerOp: 19.6,
|
||||
Measured: NsPerOp,
|
||||
},
|
||||
},
|
||||
{
|
||||
line: "BenchmarkEncrypt 100000000 19.6 ns/op 817.77 MB/s",
|
||||
want: &Benchmark{
|
||||
Name: "BenchmarkEncrypt",
|
||||
N: 100000000, NsPerOp: 19.6, MBPerS: 817.77,
|
||||
Measured: NsPerOp | MBPerS,
|
||||
},
|
||||
},
|
||||
{
|
||||
line: "BenchmarkEncrypt 100000000 19.6 ns/op 817.77",
|
||||
want: &Benchmark{
|
||||
Name: "BenchmarkEncrypt",
|
||||
N: 100000000, NsPerOp: 19.6,
|
||||
Measured: NsPerOp,
|
||||
},
|
||||
},
|
||||
{
|
||||
line: "BenchmarkEncrypt 100000000 19.6 ns/op 817.77 MB/s 5 allocs/op",
|
||||
want: &Benchmark{
|
||||
Name: "BenchmarkEncrypt",
|
||||
N: 100000000, NsPerOp: 19.6, MBPerS: 817.77, AllocsPerOp: 5,
|
||||
Measured: NsPerOp | MBPerS | AllocsPerOp,
|
||||
},
|
||||
},
|
||||
{
|
||||
line: "BenchmarkEncrypt 100000000 19.6 ns/op 817.77 MB/s 3 B/op 5 allocs/op",
|
||||
want: &Benchmark{
|
||||
Name: "BenchmarkEncrypt",
|
||||
N: 100000000, NsPerOp: 19.6, MBPerS: 817.77, AllocedBytesPerOp: 3, AllocsPerOp: 5,
|
||||
Measured: NsPerOp | MBPerS | AllocedBytesPerOp | AllocsPerOp,
|
||||
},
|
||||
},
|
||||
// error handling cases
|
||||
{
|
||||
line: "BenchPress 100 19.6 ns/op", // non-benchmark
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
line: "BenchmarkEncrypt lots 19.6 ns/op", // non-int iterations
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
line: "BenchmarkBridge 100000000 19.6 smoots", // unknown unit
|
||||
want: &Benchmark{
|
||||
Name: "BenchmarkBridge",
|
||||
N: 100000000,
|
||||
},
|
||||
},
|
||||
{
|
||||
line: "PASS",
|
||||
err: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
have, err := ParseLine(tt.line)
|
||||
if tt.err && err == nil {
|
||||
t.Errorf("parsing line %q should have failed", tt.line)
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(have, tt.want) {
|
||||
t.Errorf("parsed line %q incorrectly, want %v have %v", tt.line, tt.want, have)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSet(t *testing.T) {
|
||||
// Test two things:
|
||||
// 1. The noise that can accompany testing.B output gets ignored.
|
||||
// 2. Benchmarks with the same name have their order preserved.
|
||||
in := `
|
||||
? crypto [no test files]
|
||||
PASS
|
||||
pem_decrypt_test.go:17: test 4. %!s(x509.PEMCipher=5)
|
||||
... [output truncated]
|
||||
|
||||
BenchmarkEncrypt 100000000 19.6 ns/op
|
||||
BenchmarkEncrypt 5000000 517 ns/op
|
||||
=== RUN TestChunk
|
||||
--- PASS: TestChunk (0.00 seconds)
|
||||
--- SKIP: TestLinuxSendfile (0.00 seconds)
|
||||
fs_test.go:716: skipping; linux-only test
|
||||
BenchmarkReadRequestApachebench 1000000 2960 ns/op 27.70 MB/s 839 B/op 9 allocs/op
|
||||
BenchmarkClientServerParallel64 50000 59192 ns/op 7028 B/op 60 allocs/op
|
||||
ok net/http 95.783s
|
||||
`
|
||||
|
||||
want := Set{
|
||||
"BenchmarkReadRequestApachebench": []*Benchmark{
|
||||
{
|
||||
Name: "BenchmarkReadRequestApachebench",
|
||||
N: 1000000, NsPerOp: 2960, MBPerS: 27.70, AllocedBytesPerOp: 839, AllocsPerOp: 9,
|
||||
Measured: NsPerOp | MBPerS | AllocedBytesPerOp | AllocsPerOp,
|
||||
Ord: 2,
|
||||
},
|
||||
},
|
||||
"BenchmarkClientServerParallel64": []*Benchmark{
|
||||
{
|
||||
Name: "BenchmarkClientServerParallel64",
|
||||
N: 50000, NsPerOp: 59192, AllocedBytesPerOp: 7028, AllocsPerOp: 60,
|
||||
Measured: NsPerOp | AllocedBytesPerOp | AllocsPerOp,
|
||||
Ord: 3,
|
||||
},
|
||||
},
|
||||
"BenchmarkEncrypt": []*Benchmark{
|
||||
{
|
||||
Name: "BenchmarkEncrypt",
|
||||
N: 100000000, NsPerOp: 19.6,
|
||||
Measured: NsPerOp,
|
||||
Ord: 0,
|
||||
},
|
||||
{
|
||||
Name: "BenchmarkEncrypt",
|
||||
N: 5000000, NsPerOp: 517,
|
||||
Measured: NsPerOp,
|
||||
Ord: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
have, err := ParseSet(strings.NewReader(in))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err during ParseSet: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(want, have) {
|
||||
t.Errorf("parsed bench set incorrectly, want %v have %v", want, have)
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Adapted from encoding/xml/read_test.go.
|
||||
|
||||
// Package atom defines XML data structures for an Atom feed.
|
||||
package atom // import "golang.org/x/tools/blog/atom"
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Feed struct {
|
||||
XMLName xml.Name `xml:"http://www.w3.org/2005/Atom feed"`
|
||||
Title string `xml:"title"`
|
||||
ID string `xml:"id"`
|
||||
Link []Link `xml:"link"`
|
||||
Updated TimeStr `xml:"updated"`
|
||||
Author *Person `xml:"author"`
|
||||
Entry []*Entry `xml:"entry"`
|
||||
}
|
||||
|
||||
type Entry struct {
|
||||
Title string `xml:"title"`
|
||||
ID string `xml:"id"`
|
||||
Link []Link `xml:"link"`
|
||||
Published TimeStr `xml:"published"`
|
||||
Updated TimeStr `xml:"updated"`
|
||||
Author *Person `xml:"author"`
|
||||
Summary *Text `xml:"summary"`
|
||||
Content *Text `xml:"content"`
|
||||
}
|
||||
|
||||
type Link struct {
|
||||
Rel string `xml:"rel,attr,omitempty"`
|
||||
Href string `xml:"href,attr"`
|
||||
Type string `xml:"type,attr,omitempty"`
|
||||
HrefLang string `xml:"hreflang,attr,omitempty"`
|
||||
Title string `xml:"title,attr,omitempty"`
|
||||
Length uint `xml:"length,attr,omitempty"`
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
Name string `xml:"name"`
|
||||
URI string `xml:"uri,omitempty"`
|
||||
Email string `xml:"email,omitempty"`
|
||||
InnerXML string `xml:",innerxml"`
|
||||
}
|
||||
|
||||
type Text struct {
|
||||
Type string `xml:"type,attr"`
|
||||
Body string `xml:",chardata"`
|
||||
}
|
||||
|
||||
type TimeStr string
|
||||
|
||||
func Time(t time.Time) TimeStr {
|
||||
return TimeStr(t.Format("2006-01-02T15:04:05-07:00"))
|
||||
}
|
||||
+456
@@ -0,0 +1,456 @@
|
||||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package blog implements a web server for articles written in present format.
|
||||
package blog // import "golang.org/x/tools/blog"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/tools/blog/atom"
|
||||
"golang.org/x/tools/present"
|
||||
)
|
||||
|
||||
var (
|
||||
validJSONPFunc = regexp.MustCompile(`(?i)^[a-z_][a-z0-9_.]*$`)
|
||||
// used to serve relative paths when ServeLocalLinks is enabled.
|
||||
golangOrgAbsLinkReplacer = strings.NewReplacer(
|
||||
`href="https://golang.org/pkg`, `href="/pkg`,
|
||||
`href="https://golang.org/cmd`, `href="/cmd`,
|
||||
)
|
||||
)
|
||||
|
||||
// Config specifies Server configuration values.
|
||||
type Config struct {
|
||||
ContentPath string // Relative or absolute location of article files and related content.
|
||||
TemplatePath string // Relative or absolute location of template files.
|
||||
|
||||
BaseURL string // Absolute base URL (for permalinks; no trailing slash).
|
||||
BasePath string // Base URL path relative to server root (no trailing slash).
|
||||
GodocURL string // The base URL of godoc (for menu bar; no trailing slash).
|
||||
Hostname string // Server host name, used for rendering ATOM feeds.
|
||||
|
||||
HomeArticles int // Articles to display on the home page.
|
||||
FeedArticles int // Articles to include in Atom and JSON feeds.
|
||||
FeedTitle string // The title of the Atom XML feed
|
||||
|
||||
PlayEnabled bool
|
||||
ServeLocalLinks bool // rewrite golang.org/{pkg,cmd} links to host-less, relative paths.
|
||||
}
|
||||
|
||||
// Doc represents an article adorned with presentation data.
|
||||
type Doc struct {
|
||||
*present.Doc
|
||||
Permalink string // Canonical URL for this document.
|
||||
Path string // Path relative to server root (including base).
|
||||
HTML template.HTML // rendered article
|
||||
|
||||
Related []*Doc
|
||||
Newer, Older *Doc
|
||||
}
|
||||
|
||||
// Server implements an http.Handler that serves blog articles.
|
||||
type Server struct {
|
||||
cfg Config
|
||||
docs []*Doc
|
||||
tags []string
|
||||
docPaths map[string]*Doc // key is path without BasePath.
|
||||
docTags map[string][]*Doc
|
||||
template struct {
|
||||
home, index, article, doc *template.Template
|
||||
}
|
||||
atomFeed []byte // pre-rendered Atom feed
|
||||
jsonFeed []byte // pre-rendered JSON feed
|
||||
content http.Handler
|
||||
}
|
||||
|
||||
// NewServer constructs a new Server using the specified config.
|
||||
func NewServer(cfg Config) (*Server, error) {
|
||||
present.PlayEnabled = cfg.PlayEnabled
|
||||
|
||||
if notExist(cfg.TemplatePath) {
|
||||
return nil, fmt.Errorf("template directory not found: %s", cfg.TemplatePath)
|
||||
}
|
||||
root := filepath.Join(cfg.TemplatePath, "root.tmpl")
|
||||
parse := func(name string) (*template.Template, error) {
|
||||
path := filepath.Join(cfg.TemplatePath, name)
|
||||
if notExist(path) {
|
||||
return nil, fmt.Errorf("template %s was not found in %s", name, cfg.TemplatePath)
|
||||
}
|
||||
t := template.New("").Funcs(funcMap)
|
||||
return t.ParseFiles(root, path)
|
||||
}
|
||||
|
||||
s := &Server{cfg: cfg}
|
||||
|
||||
// Parse templates.
|
||||
var err error
|
||||
s.template.home, err = parse("home.tmpl")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.template.index, err = parse("index.tmpl")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.template.article, err = parse("article.tmpl")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p := present.Template().Funcs(funcMap)
|
||||
s.template.doc, err = p.ParseFiles(filepath.Join(cfg.TemplatePath, "doc.tmpl"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Load content.
|
||||
err = s.loadDocs(filepath.Clean(cfg.ContentPath))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.renderAtomFeed()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.renderJSONFeed()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Set up content file server.
|
||||
s.content = http.StripPrefix(s.cfg.BasePath, http.FileServer(http.Dir(cfg.ContentPath)))
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
var funcMap = template.FuncMap{
|
||||
"sectioned": sectioned,
|
||||
"authors": authors,
|
||||
}
|
||||
|
||||
// sectioned returns true if the provided Doc contains more than one section.
|
||||
// This is used to control whether to display the table of contents and headings.
|
||||
func sectioned(d *present.Doc) bool {
|
||||
return len(d.Sections) > 1
|
||||
}
|
||||
|
||||
// authors returns a comma-separated list of author names.
|
||||
func authors(authors []present.Author) string {
|
||||
var b bytes.Buffer
|
||||
last := len(authors) - 1
|
||||
for i, a := range authors {
|
||||
if i > 0 {
|
||||
if i == last {
|
||||
b.WriteString(" and ")
|
||||
} else {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
}
|
||||
b.WriteString(authorName(a))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// authorName returns the first line of the Author text: the author's name.
|
||||
func authorName(a present.Author) string {
|
||||
el := a.TextElem()
|
||||
if len(el) == 0 {
|
||||
return ""
|
||||
}
|
||||
text, ok := el[0].(present.Text)
|
||||
if !ok || len(text.Lines) == 0 {
|
||||
return ""
|
||||
}
|
||||
return text.Lines[0]
|
||||
}
|
||||
|
||||
// loadDocs reads all content from the provided file system root, renders all
|
||||
// the articles it finds, adds them to the Server's docs field, computes the
|
||||
// denormalized docPaths, docTags, and tags fields, and populates the various
|
||||
// helper fields (Next, Previous, Related) for each Doc.
|
||||
func (s *Server) loadDocs(root string) error {
|
||||
// Read content into docs field.
|
||||
const ext = ".article"
|
||||
fn := func(p string, info os.FileInfo, err error) error {
|
||||
if filepath.Ext(p) != ext {
|
||||
return nil
|
||||
}
|
||||
f, err := os.Open(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
d, err := present.Parse(f, p, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var html bytes.Buffer
|
||||
err = d.Render(&html, s.template.doc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p = p[len(root) : len(p)-len(ext)] // trim root and extension
|
||||
p = filepath.ToSlash(p)
|
||||
s.docs = append(s.docs, &Doc{
|
||||
Doc: d,
|
||||
Path: s.cfg.BasePath + p,
|
||||
Permalink: s.cfg.BaseURL + p,
|
||||
HTML: template.HTML(html.String()),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
err := filepath.Walk(root, fn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sort.Sort(docsByTime(s.docs))
|
||||
|
||||
// Pull out doc paths and tags and put in reverse-associating maps.
|
||||
s.docPaths = make(map[string]*Doc)
|
||||
s.docTags = make(map[string][]*Doc)
|
||||
for _, d := range s.docs {
|
||||
s.docPaths[strings.TrimPrefix(d.Path, s.cfg.BasePath)] = d
|
||||
for _, t := range d.Tags {
|
||||
s.docTags[t] = append(s.docTags[t], d)
|
||||
}
|
||||
}
|
||||
|
||||
// Pull out unique sorted list of tags.
|
||||
for t := range s.docTags {
|
||||
s.tags = append(s.tags, t)
|
||||
}
|
||||
sort.Strings(s.tags)
|
||||
|
||||
// Set up presentation-related fields, Newer, Older, and Related.
|
||||
for _, doc := range s.docs {
|
||||
// Newer, Older: docs adjacent to doc
|
||||
for i := range s.docs {
|
||||
if s.docs[i] != doc {
|
||||
continue
|
||||
}
|
||||
if i > 0 {
|
||||
doc.Newer = s.docs[i-1]
|
||||
}
|
||||
if i+1 < len(s.docs) {
|
||||
doc.Older = s.docs[i+1]
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Related: all docs that share tags with doc.
|
||||
related := make(map[*Doc]bool)
|
||||
for _, t := range doc.Tags {
|
||||
for _, d := range s.docTags[t] {
|
||||
if d != doc {
|
||||
related[d] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
for d := range related {
|
||||
doc.Related = append(doc.Related, d)
|
||||
}
|
||||
sort.Sort(docsByTime(doc.Related))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// renderAtomFeed generates an XML Atom feed and stores it in the Server's
|
||||
// atomFeed field.
|
||||
func (s *Server) renderAtomFeed() error {
|
||||
var updated time.Time
|
||||
if len(s.docs) > 0 {
|
||||
updated = s.docs[0].Time
|
||||
}
|
||||
feed := atom.Feed{
|
||||
Title: s.cfg.FeedTitle,
|
||||
ID: "tag:" + s.cfg.Hostname + ",2013:" + s.cfg.Hostname,
|
||||
Updated: atom.Time(updated),
|
||||
Link: []atom.Link{{
|
||||
Rel: "self",
|
||||
Href: s.cfg.BaseURL + "/feed.atom",
|
||||
}},
|
||||
}
|
||||
for i, doc := range s.docs {
|
||||
if i >= s.cfg.FeedArticles {
|
||||
break
|
||||
}
|
||||
e := &atom.Entry{
|
||||
Title: doc.Title,
|
||||
ID: feed.ID + doc.Path,
|
||||
Link: []atom.Link{{
|
||||
Rel: "alternate",
|
||||
Href: doc.Permalink,
|
||||
}},
|
||||
Published: atom.Time(doc.Time),
|
||||
Updated: atom.Time(doc.Time),
|
||||
Summary: &atom.Text{
|
||||
Type: "html",
|
||||
Body: summary(doc),
|
||||
},
|
||||
Content: &atom.Text{
|
||||
Type: "html",
|
||||
Body: string(doc.HTML),
|
||||
},
|
||||
Author: &atom.Person{
|
||||
Name: authors(doc.Authors),
|
||||
},
|
||||
}
|
||||
feed.Entry = append(feed.Entry, e)
|
||||
}
|
||||
data, err := xml.Marshal(&feed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.atomFeed = data
|
||||
return nil
|
||||
}
|
||||
|
||||
type jsonItem struct {
|
||||
Title string
|
||||
Link string
|
||||
Time time.Time
|
||||
Summary string
|
||||
Content string
|
||||
Author string
|
||||
}
|
||||
|
||||
// renderJSONFeed generates a JSON feed and stores it in the Server's jsonFeed
|
||||
// field.
|
||||
func (s *Server) renderJSONFeed() error {
|
||||
var feed []jsonItem
|
||||
for i, doc := range s.docs {
|
||||
if i >= s.cfg.FeedArticles {
|
||||
break
|
||||
}
|
||||
item := jsonItem{
|
||||
Title: doc.Title,
|
||||
Link: doc.Permalink,
|
||||
Time: doc.Time,
|
||||
Summary: summary(doc),
|
||||
Content: string(doc.HTML),
|
||||
Author: authors(doc.Authors),
|
||||
}
|
||||
feed = append(feed, item)
|
||||
}
|
||||
data, err := json.Marshal(feed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.jsonFeed = data
|
||||
return nil
|
||||
}
|
||||
|
||||
// summary returns the first paragraph of text from the provided Doc.
|
||||
func summary(d *Doc) string {
|
||||
if len(d.Sections) == 0 {
|
||||
return ""
|
||||
}
|
||||
for _, elem := range d.Sections[0].Elem {
|
||||
text, ok := elem.(present.Text)
|
||||
if !ok || text.Pre {
|
||||
// skip everything but non-text elements
|
||||
continue
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
for _, s := range text.Lines {
|
||||
buf.WriteString(string(present.Style(s)))
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// rootData encapsulates data destined for the root template.
|
||||
type rootData struct {
|
||||
Doc *Doc
|
||||
BasePath string
|
||||
GodocURL string
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
// ServeHTTP serves the front, index, and article pages
|
||||
// as well as the ATOM and JSON feeds.
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
d = rootData{BasePath: s.cfg.BasePath, GodocURL: s.cfg.GodocURL}
|
||||
t *template.Template
|
||||
)
|
||||
switch p := strings.TrimPrefix(r.URL.Path, s.cfg.BasePath); p {
|
||||
case "/":
|
||||
d.Data = s.docs
|
||||
if len(s.docs) > s.cfg.HomeArticles {
|
||||
d.Data = s.docs[:s.cfg.HomeArticles]
|
||||
}
|
||||
t = s.template.home
|
||||
case "/index":
|
||||
d.Data = s.docs
|
||||
t = s.template.index
|
||||
case "/feed.atom", "/feeds/posts/default":
|
||||
w.Header().Set("Content-type", "application/atom+xml; charset=utf-8")
|
||||
w.Write(s.atomFeed)
|
||||
return
|
||||
case "/.json":
|
||||
if p := r.FormValue("jsonp"); validJSONPFunc.MatchString(p) {
|
||||
w.Header().Set("Content-type", "application/javascript; charset=utf-8")
|
||||
fmt.Fprintf(w, "%v(%s)", p, s.jsonFeed)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-type", "application/json; charset=utf-8")
|
||||
w.Write(s.jsonFeed)
|
||||
return
|
||||
default:
|
||||
doc, ok := s.docPaths[p]
|
||||
if !ok {
|
||||
// Not a doc; try to just serve static content.
|
||||
s.content.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
d.Doc = doc
|
||||
t = s.template.article
|
||||
}
|
||||
var err error
|
||||
if s.cfg.ServeLocalLinks {
|
||||
var buf bytes.Buffer
|
||||
err = t.ExecuteTemplate(&buf, "root", d)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
_, err = golangOrgAbsLinkReplacer.WriteString(w, buf.String())
|
||||
} else {
|
||||
err = t.ExecuteTemplate(w, "root", d)
|
||||
}
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// docsByTime implements sort.Interface, sorting Docs by their Time field.
|
||||
type docsByTime []*Doc
|
||||
|
||||
func (s docsByTime) Len() int { return len(s) }
|
||||
func (s docsByTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
func (s docsByTime) Less(i, j int) bool { return s[i].Time.After(s[j].Time) }
|
||||
|
||||
// notExist reports whether the path exists or not.
|
||||
func notExist(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return os.IsNotExist(err)
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
// Copyright 2018 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package blog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLinkRewrite(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
output string
|
||||
}{
|
||||
{
|
||||
`For instance, the <a href="https://golang.org/pkg/bytes/" target="_blank">bytes package</a> from the standard library exports the <code>Buffer</code> type.`,
|
||||
`For instance, the <a href="/pkg/bytes/" target="_blank">bytes package</a> from the standard library exports the <code>Buffer</code> type.`},
|
||||
{
|
||||
`(The <a href="https://golang.org/cmd/gofmt/" target="_blank">gofmt command</a> has a <code>-r</code> flag that provides a syntax-aware search and replace, making large-scale refactoring easier.)`,
|
||||
`(The <a href="/cmd/gofmt/" target="_blank">gofmt command</a> has a <code>-r</code> flag that provides a syntax-aware search and replace, making large-scale refactoring easier.)`,
|
||||
},
|
||||
{
|
||||
`<a href="//golang.org/LICENSE">BSD license</a>.<br> <a href="//golang.org/doc/tos.html">Terms of Service</a> `,
|
||||
`<a href="//golang.org/LICENSE">BSD license</a>.<br> <a href="//golang.org/doc/tos.html">Terms of Service</a> `,
|
||||
},
|
||||
{
|
||||
`For instance, the <code>websocket</code> package from the <code>go.net</code> sub-repository has an import path of <code>"golang.org/x/net/websocket"</code>.`,
|
||||
`For instance, the <code>websocket</code> package from the <code>go.net</code> sub-repository has an import path of <code>"golang.org/x/net/websocket"</code>.`,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
var buf bytes.Buffer
|
||||
_, err := golangOrgAbsLinkReplacer.WriteString(&buf, test.input)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error during replacing links. Got: %#v, Want: nil.\n", err)
|
||||
continue
|
||||
}
|
||||
if got, want := buf.String(), test.output; got != want {
|
||||
t.Errorf("WriteString(%q) = %q. Expected: %q", test.input, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"text/tabwriter"
|
||||
|
||||
"golang.org/x/tools/benchmark/parse"
|
||||
)
|
||||
|
||||
var (
|
||||
changedOnly = flag.Bool("changed", false, "show only benchmarks that have changed")
|
||||
magSort = flag.Bool("mag", false, "sort benchmarks by magnitude of change")
|
||||
best = flag.Bool("best", false, "compare best times from old and new")
|
||||
)
|
||||
|
||||
const usageFooter = `
|
||||
Each input file should be from:
|
||||
go test -run=NONE -bench=. > [old,new].txt
|
||||
|
||||
Benchcmp compares old and new for each benchmark.
|
||||
|
||||
If -test.benchmem=true is added to the "go test" command
|
||||
benchcmp will also compare memory allocations.
|
||||
`
|
||||
|
||||
func main() {
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(os.Stderr, "usage: %s old.txt new.txt\n\n", os.Args[0])
|
||||
flag.PrintDefaults()
|
||||
fmt.Fprint(os.Stderr, usageFooter)
|
||||
os.Exit(2)
|
||||
}
|
||||
flag.Parse()
|
||||
if flag.NArg() != 2 {
|
||||
flag.Usage()
|
||||
}
|
||||
|
||||
before := parseFile(flag.Arg(0))
|
||||
after := parseFile(flag.Arg(1))
|
||||
|
||||
cmps, warnings := Correlate(before, after)
|
||||
|
||||
for _, warn := range warnings {
|
||||
fmt.Fprintln(os.Stderr, warn)
|
||||
}
|
||||
|
||||
if len(cmps) == 0 {
|
||||
fatal("benchcmp: no repeated benchmarks")
|
||||
}
|
||||
|
||||
w := new(tabwriter.Writer)
|
||||
w.Init(os.Stdout, 0, 0, 5, ' ', 0)
|
||||
defer w.Flush()
|
||||
|
||||
var header bool // Has the header has been displayed yet for a given block?
|
||||
|
||||
if *magSort {
|
||||
sort.Sort(ByDeltaNsPerOp(cmps))
|
||||
} else {
|
||||
sort.Sort(ByParseOrder(cmps))
|
||||
}
|
||||
for _, cmp := range cmps {
|
||||
if !cmp.Measured(parse.NsPerOp) {
|
||||
continue
|
||||
}
|
||||
if delta := cmp.DeltaNsPerOp(); !*changedOnly || delta.Changed() {
|
||||
if !header {
|
||||
fmt.Fprint(w, "benchmark\told ns/op\tnew ns/op\tdelta\n")
|
||||
header = true
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", cmp.Name(), formatNs(cmp.Before.NsPerOp), formatNs(cmp.After.NsPerOp), delta.Percent())
|
||||
}
|
||||
}
|
||||
|
||||
header = false
|
||||
if *magSort {
|
||||
sort.Sort(ByDeltaMBPerS(cmps))
|
||||
}
|
||||
for _, cmp := range cmps {
|
||||
if !cmp.Measured(parse.MBPerS) {
|
||||
continue
|
||||
}
|
||||
if delta := cmp.DeltaMBPerS(); !*changedOnly || delta.Changed() {
|
||||
if !header {
|
||||
fmt.Fprint(w, "\nbenchmark\told MB/s\tnew MB/s\tspeedup\n")
|
||||
header = true
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%.2f\t%.2f\t%s\n", cmp.Name(), cmp.Before.MBPerS, cmp.After.MBPerS, delta.Multiple())
|
||||
}
|
||||
}
|
||||
|
||||
header = false
|
||||
if *magSort {
|
||||
sort.Sort(ByDeltaAllocsPerOp(cmps))
|
||||
}
|
||||
for _, cmp := range cmps {
|
||||
if !cmp.Measured(parse.AllocsPerOp) {
|
||||
continue
|
||||
}
|
||||
if delta := cmp.DeltaAllocsPerOp(); !*changedOnly || delta.Changed() {
|
||||
if !header {
|
||||
fmt.Fprint(w, "\nbenchmark\told allocs\tnew allocs\tdelta\n")
|
||||
header = true
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%d\t%d\t%s\n", cmp.Name(), cmp.Before.AllocsPerOp, cmp.After.AllocsPerOp, delta.Percent())
|
||||
}
|
||||
}
|
||||
|
||||
header = false
|
||||
if *magSort {
|
||||
sort.Sort(ByDeltaAllocedBytesPerOp(cmps))
|
||||
}
|
||||
for _, cmp := range cmps {
|
||||
if !cmp.Measured(parse.AllocedBytesPerOp) {
|
||||
continue
|
||||
}
|
||||
if delta := cmp.DeltaAllocedBytesPerOp(); !*changedOnly || delta.Changed() {
|
||||
if !header {
|
||||
fmt.Fprint(w, "\nbenchmark\told bytes\tnew bytes\tdelta\n")
|
||||
header = true
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%d\t%d\t%s\n", cmp.Name(), cmp.Before.AllocedBytesPerOp, cmp.After.AllocedBytesPerOp, cmp.DeltaAllocedBytesPerOp().Percent())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fatal(msg interface{}) {
|
||||
fmt.Fprintln(os.Stderr, msg)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func parseFile(path string) parse.Set {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
bb, err := parse.ParseSet(f)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
if *best {
|
||||
selectBest(bb)
|
||||
}
|
||||
return bb
|
||||
}
|
||||
|
||||
func selectBest(bs parse.Set) {
|
||||
for name, bb := range bs {
|
||||
if len(bb) < 2 {
|
||||
continue
|
||||
}
|
||||
ord := bb[0].Ord
|
||||
best := bb[0]
|
||||
for _, b := range bb {
|
||||
if b.NsPerOp < best.NsPerOp {
|
||||
b.Ord = ord
|
||||
best = b
|
||||
}
|
||||
}
|
||||
bs[name] = []*parse.Benchmark{best}
|
||||
}
|
||||
}
|
||||
|
||||
// formatNs formats ns measurements to expose a useful amount of
|
||||
// precision. It mirrors the ns precision logic of testing.B.
|
||||
func formatNs(ns float64) string {
|
||||
prec := 0
|
||||
switch {
|
||||
case ns < 10:
|
||||
prec = 2
|
||||
case ns < 100:
|
||||
prec = 1
|
||||
}
|
||||
return strconv.FormatFloat(ns, 'f', prec, 64)
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/tools/benchmark/parse"
|
||||
)
|
||||
|
||||
func TestSelectBest(t *testing.T) {
|
||||
have := parse.Set{
|
||||
"Benchmark1": []*parse.Benchmark{
|
||||
{
|
||||
Name: "Benchmark1",
|
||||
N: 10, NsPerOp: 100, Measured: parse.NsPerOp,
|
||||
Ord: 0,
|
||||
},
|
||||
{
|
||||
Name: "Benchmark1",
|
||||
N: 10, NsPerOp: 50, Measured: parse.NsPerOp,
|
||||
Ord: 3,
|
||||
},
|
||||
},
|
||||
"Benchmark2": []*parse.Benchmark{
|
||||
{
|
||||
Name: "Benchmark2",
|
||||
N: 10, NsPerOp: 60, Measured: parse.NsPerOp,
|
||||
Ord: 1,
|
||||
},
|
||||
{
|
||||
Name: "Benchmark2",
|
||||
N: 10, NsPerOp: 500, Measured: parse.NsPerOp,
|
||||
Ord: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
want := parse.Set{
|
||||
"Benchmark1": []*parse.Benchmark{
|
||||
{
|
||||
Name: "Benchmark1",
|
||||
N: 10, NsPerOp: 50, Measured: parse.NsPerOp,
|
||||
Ord: 0,
|
||||
},
|
||||
},
|
||||
"Benchmark2": []*parse.Benchmark{
|
||||
{
|
||||
Name: "Benchmark2",
|
||||
N: 10, NsPerOp: 60, Measured: parse.NsPerOp,
|
||||
Ord: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
selectBest(have)
|
||||
if !reflect.DeepEqual(want, have) {
|
||||
t.Errorf("filtered bench set incorrectly, want %v have %v", want, have)
|
||||
}
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
// Copyright 2014 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"golang.org/x/tools/benchmark/parse"
|
||||
)
|
||||
|
||||
// BenchCmp is a pair of benchmarks.
|
||||
type BenchCmp struct {
|
||||
Before *parse.Benchmark
|
||||
After *parse.Benchmark
|
||||
}
|
||||
|
||||
// Correlate correlates benchmarks from two BenchSets.
|
||||
func Correlate(before, after parse.Set) (cmps []BenchCmp, warnings []string) {
|
||||
cmps = make([]BenchCmp, 0, len(after))
|
||||
for name, beforebb := range before {
|
||||
afterbb := after[name]
|
||||
if len(beforebb) != len(afterbb) {
|
||||
warnings = append(warnings, fmt.Sprintf("ignoring %s: before has %d instances, after has %d", name, len(beforebb), len(afterbb)))
|
||||
continue
|
||||
}
|
||||
for i, beforeb := range beforebb {
|
||||
afterb := afterbb[i]
|
||||
cmps = append(cmps, BenchCmp{beforeb, afterb})
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c BenchCmp) Name() string { return c.Before.Name }
|
||||
func (c BenchCmp) String() string { return fmt.Sprintf("<%s, %s>", c.Before, c.After) }
|
||||
func (c BenchCmp) Measured(flag int) bool { return (c.Before.Measured & c.After.Measured & flag) != 0 }
|
||||
func (c BenchCmp) DeltaNsPerOp() Delta { return Delta{c.Before.NsPerOp, c.After.NsPerOp} }
|
||||
func (c BenchCmp) DeltaMBPerS() Delta { return Delta{c.Before.MBPerS, c.After.MBPerS} }
|
||||
func (c BenchCmp) DeltaAllocedBytesPerOp() Delta {
|
||||
return Delta{float64(c.Before.AllocedBytesPerOp), float64(c.After.AllocedBytesPerOp)}
|
||||
}
|
||||
func (c BenchCmp) DeltaAllocsPerOp() Delta {
|
||||
return Delta{float64(c.Before.AllocsPerOp), float64(c.After.AllocsPerOp)}
|
||||
}
|
||||
|
||||
// Delta is the before and after value for a benchmark measurement.
|
||||
// Both must be non-negative.
|
||||
type Delta struct {
|
||||
Before float64
|
||||
After float64
|
||||
}
|
||||
|
||||
// mag calculates the magnitude of a change, regardless of the direction of
|
||||
// the change. mag is intended for sorting and has no independent meaning.
|
||||
func (d Delta) mag() float64 {
|
||||
switch {
|
||||
case d.Before != 0 && d.After != 0 && d.Before >= d.After:
|
||||
return d.After / d.Before
|
||||
case d.Before != 0 && d.After != 0 && d.Before < d.After:
|
||||
return d.Before / d.After
|
||||
case d.Before == 0 && d.After == 0:
|
||||
return 1
|
||||
default:
|
||||
// 0 -> 1 or 1 -> 0
|
||||
// These are significant changes and worth surfacing.
|
||||
return math.Inf(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Changed reports whether the benchmark quantities are different.
|
||||
func (d Delta) Changed() bool { return d.Before != d.After }
|
||||
|
||||
// Float64 returns After / Before. If Before is 0, Float64 returns
|
||||
// 1 if After is also 0, and +Inf otherwise.
|
||||
func (d Delta) Float64() float64 {
|
||||
switch {
|
||||
case d.Before != 0:
|
||||
return d.After / d.Before
|
||||
case d.After == 0:
|
||||
return 1
|
||||
default:
|
||||
return math.Inf(1)
|
||||
}
|
||||
}
|
||||
|
||||
// Percent formats a Delta as a percent change, ranging from -100% up.
|
||||
func (d Delta) Percent() string {
|
||||
return fmt.Sprintf("%+.2f%%", 100*d.Float64()-100)
|
||||
}
|
||||
|
||||
// Multiple formats a Delta as a multiplier, ranging from 0.00x up.
|
||||
func (d Delta) Multiple() string {
|
||||
return fmt.Sprintf("%.2fx", d.Float64())
|
||||
}
|
||||
|
||||
func (d Delta) String() string {
|
||||
return fmt.Sprintf("Δ(%f, %f)", d.Before, d.After)
|
||||
}
|
||||
|
||||
// ByParseOrder sorts BenchCmps to match the order in
|
||||
// which the Before benchmarks were presented to Parse.
|
||||
type ByParseOrder []BenchCmp
|
||||
|
||||
func (x ByParseOrder) Len() int { return len(x) }
|
||||
func (x ByParseOrder) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
|
||||
func (x ByParseOrder) Less(i, j int) bool { return x[i].Before.Ord < x[j].Before.Ord }
|
||||
|
||||
// lessByDelta provides lexicographic ordering:
|
||||
// * largest delta by magnitude
|
||||
// * alphabetic by name
|
||||
func lessByDelta(i, j BenchCmp, calcDelta func(BenchCmp) Delta) bool {
|
||||
iDelta, jDelta := calcDelta(i).mag(), calcDelta(j).mag()
|
||||
if iDelta != jDelta {
|
||||
return iDelta < jDelta
|
||||
}
|
||||
return i.Name() < j.Name()
|
||||
}
|
||||
|
||||
// ByDeltaNsPerOp sorts BenchCmps lexicographically by change
|
||||
// in ns/op, descending, then by benchmark name.
|
||||
type ByDeltaNsPerOp []BenchCmp
|
||||
|
||||
func (x ByDeltaNsPerOp) Len() int { return len(x) }
|
||||
func (x ByDeltaNsPerOp) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
|
||||
func (x ByDeltaNsPerOp) Less(i, j int) bool { return lessByDelta(x[i], x[j], BenchCmp.DeltaNsPerOp) }
|
||||
|
||||
// ByDeltaMBPerS sorts BenchCmps lexicographically by change
|
||||
// in MB/s, descending, then by benchmark name.
|
||||
type ByDeltaMBPerS []BenchCmp
|
||||
|
||||
func (x ByDeltaMBPerS) Len() int { return len(x) }
|
||||
func (x ByDeltaMBPerS) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
|
||||
func (x ByDeltaMBPerS) Less(i, j int) bool { return lessByDelta(x[i], x[j], BenchCmp.DeltaMBPerS) }
|
||||
|
||||
// ByDeltaAllocedBytesPerOp sorts BenchCmps lexicographically by change
|
||||
// in B/op, descending, then by benchmark name.
|
||||
type ByDeltaAllocedBytesPerOp []BenchCmp
|
||||
|
||||
func (x ByDeltaAllocedBytesPerOp) Len() int { return len(x) }
|
||||
func (x ByDeltaAllocedBytesPerOp) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
|
||||
func (x ByDeltaAllocedBytesPerOp) Less(i, j int) bool {
|
||||
return lessByDelta(x[i], x[j], BenchCmp.DeltaAllocedBytesPerOp)
|
||||
}
|
||||
|
||||
// ByDeltaAllocsPerOp sorts BenchCmps lexicographically by change
|
||||
// in allocs/op, descending, then by benchmark name.
|
||||
type ByDeltaAllocsPerOp []BenchCmp
|
||||
|
||||
func (x ByDeltaAllocsPerOp) Len() int { return len(x) }
|
||||
func (x ByDeltaAllocsPerOp) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
|
||||
func (x ByDeltaAllocsPerOp) Less(i, j int) bool {
|
||||
return lessByDelta(x[i], x[j], BenchCmp.DeltaAllocsPerOp)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user