Merge pull request #737 from weaveworks/284-probe-cpu

Move procspy out of vendor into probe/endpoint.
This commit is contained in:
Tom Wilkie
2015-12-10 13:59:12 +00:00
23 changed files with 304 additions and 165 deletions

11
common/fs/fs.go Normal file
View File

@@ -0,0 +1,11 @@
package fs
import (
"io"
"os"
)
// Open is a mockable version of os.Open
var Open = func(path string) (io.ReadWriteCloser, error) {
return os.Open(path)
}

View File

@@ -12,8 +12,8 @@ func TestLSOFParsing(t *testing.T) {
"p25196\n" +
"ccello-app\n" +
"n127.0.0.1:48094->127.0.0.1:4039\n" +
"n*:4040\n": map[string]Proc{
"127.0.0.1:48094": Proc{
"n*:4040\n": {
"127.0.0.1:48094": {
PID: 25196,
Name: "cello-app",
},
@@ -23,7 +23,7 @@ func TestLSOFParsing(t *testing.T) {
"cdhclient\n" +
"n*:68\n" +
"n*:38282\n" +
"n*:40625\n": map[string]Proc{},
"n*:40625\n": {},
// A bunch
"p13100\n" +
@@ -39,28 +39,28 @@ func TestLSOFParsing(t *testing.T) {
"n192.168.2.111:56385->74.201.105.31:443\n" +
"p21356\n" +
"cssh\n" +
"n192.168.2.111:33963->192.168.2.71:22\n": map[string]Proc{
"[::1]:6600": Proc{
"n192.168.2.111:33963->192.168.2.71:22\n": {
"[::1]:6600": {
PID: 13100,
Name: "mpd",
},
"[2003:45:2b57:8900:1869:2947:f942:aba7]:55711": Proc{
"[2003:45:2b57:8900:1869:2947:f942:aba7]:55711": {
PID: 14612,
Name: "chromium",
},
"192.168.2.111:37158": Proc{
"192.168.2.111:37158": {
PID: 14612,
Name: "chromium",
},
"192.168.2.111:44013": Proc{
"192.168.2.111:44013": {
PID: 14612,
Name: "chromium",
},
"192.168.2.111:56385": Proc{
"192.168.2.111:56385": {
PID: 14612,
Name: "chromium",
},
"192.168.2.111:33963": Proc{
"192.168.2.111:33963": {
PID: 21356,
Name: "ssh",
},

View File

@@ -4,10 +4,13 @@ package procspy
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"syscall"
"github.com/weaveworks/scope/common/fs"
)
var (
@@ -19,17 +22,19 @@ func SetProcRoot(root string) {
procRoot = root
}
// made variables for mocking
var (
readDir = ioutil.ReadDir
lstat = syscall.Lstat
stat = syscall.Stat
open = fs.Open
)
// walkProcPid walks over all numerical (PID) /proc entries, and sees if their
// ./fd/* files are symlink to sockets. Returns a map from socket ID (inode)
// to PID. Will return an error if /proc isn't there.
func walkProcPid(buf *bytes.Buffer) (map[uint64]Proc, error) {
fh, err := os.Open(procRoot)
if err != nil {
return nil, err
}
dirNames, err := fh.Readdirnames(-1)
fh.Close()
dirNames, err := readDir(procRoot)
if err != nil {
return nil, err
}
@@ -37,9 +42,10 @@ func walkProcPid(buf *bytes.Buffer) (map[uint64]Proc, error) {
var (
res = map[uint64]Proc{}
namespaces = map[uint64]struct{}{}
stat syscall.Stat_t
statT syscall.Stat_t
)
for _, dirName := range dirNames {
for _, entry := range dirNames {
dirName := entry.Name()
pid, err := strconv.ParseUint(dirName, 10, 0)
if err != nil {
// Not a number, so not a PID subdir.
@@ -47,41 +53,35 @@ func walkProcPid(buf *bytes.Buffer) (map[uint64]Proc, error) {
}
fdBase := filepath.Join(procRoot, dirName, "fd")
dfh, err := os.Open(fdBase)
fds, err := readDir(fdBase)
if err != nil {
// Process is be gone by now, or we don't have access.
continue
}
fdNames, err := dfh.Readdirnames(-1)
dfh.Close()
if err != nil {
continue
}
// Read network namespace, and if we haven't seen it before,
// read /proc/<pid>/net/tcp
err = syscall.Lstat(filepath.Join(procRoot, dirName, "/ns/net"), &stat)
err = lstat(filepath.Join(procRoot, dirName, "/ns/net"), &statT)
if err != nil {
continue
}
if _, ok := namespaces[stat.Ino]; !ok {
namespaces[stat.Ino] = struct{}{}
if _, ok := namespaces[statT.Ino]; !ok {
namespaces[statT.Ino] = struct{}{}
readFile(filepath.Join(procRoot, dirName, "/net/tcp"), buf)
readFile(filepath.Join(procRoot, dirName, "/net/tcp6"), buf)
}
var name string
for _, fdName := range fdNames {
for _, fd := range fds {
// Direct use of syscall.Stat() to save garbage.
err = syscall.Stat(filepath.Join(fdBase, fdName), &stat)
err = stat(filepath.Join(fdBase, fd.Name()), &statT)
if err != nil {
continue
}
// We want sockets only.
if stat.Mode&syscall.S_IFMT != syscall.S_IFSOCK {
if statT.Mode&syscall.S_IFMT != syscall.S_IFSOCK {
continue
}
@@ -92,7 +92,7 @@ func walkProcPid(buf *bytes.Buffer) (map[uint64]Proc, error) {
}
}
res[stat.Ino] = Proc{
res[statT.Ino] = Proc{
PID: uint(pid),
Name: name,
}
@@ -104,7 +104,7 @@ func walkProcPid(buf *bytes.Buffer) (map[uint64]Proc, error) {
// procName does a pid->name lookup.
func procName(base string) string {
fh, err := os.Open(filepath.Join(base, "/comm"))
fh, err := open(filepath.Join(base, "/comm"))
if err != nil {
return ""
}

View File

@@ -0,0 +1,57 @@
package procspy
import (
"bytes"
"reflect"
"syscall"
"testing"
"github.com/weaveworks/scope/test/fs"
)
var mockFS = fs.Dir("",
fs.Dir("proc",
fs.Dir("1",
fs.Dir("fd",
fs.File{
FName: "16",
FStat: syscall.Stat_t{
Ino: 45,
Mode: syscall.S_IFSOCK,
},
},
),
fs.File{
FName: "comm",
FContents: "foo\n",
},
fs.Dir("ns",
fs.File{
FName: "net",
FStat: syscall.Stat_t{},
},
),
),
),
)
func TestWalkProcPid(t *testing.T) {
oldReadDir, oldLstat, oldStat, oldOpen := readDir, lstat, stat, open
defer func() { readDir, lstat, stat, open = oldReadDir, oldLstat, oldStat, oldOpen }()
readDir, lstat, stat, open = mockFS.ReadDir, mockFS.Lstat, mockFS.Stat, mockFS.Open
buf := bytes.Buffer{}
have, err := walkProcPid(&buf)
if err != nil {
t.Fatal(err)
}
want := map[uint64]Proc{
45: {
PID: 1,
Name: "foo",
},
}
if !reflect.DeepEqual(want, have) {
t.Fatalf("%+v", have)
}
}

View File

@@ -6,7 +6,7 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/weaveworks/procspy"
"github.com/weaveworks/scope/probe/endpoint/procspy"
"github.com/weaveworks/scope/probe/process"
"github.com/weaveworks/scope/report"
)

View File

@@ -5,9 +5,9 @@ import (
"strconv"
"testing"
"github.com/weaveworks/procspy"
"github.com/weaveworks/scope/probe/docker"
"github.com/weaveworks/scope/probe/endpoint"
"github.com/weaveworks/scope/probe/endpoint/procspy"
"github.com/weaveworks/scope/report"
)

199
test/fs/fs.go Normal file
View File

@@ -0,0 +1,199 @@
package fs
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"syscall"
"time"
)
type mockInode struct{}
type dir struct {
mockInode
name string
entries map[string]FS
stat syscall.Stat_t
}
// File is a mock file
type File struct {
mockInode
FName string
FContents string
FStat syscall.Stat_t
}
// FS is a mock filesystem
type FS interface {
os.FileInfo
ReadDir(string) ([]os.FileInfo, error)
ReadFile(string) ([]byte, error)
Lstat(string, *syscall.Stat_t) error
Stat(string, *syscall.Stat_t) error
Open(string) (io.ReadWriteCloser, error)
}
// Dir creates a new directory with the given entries.
func Dir(name string, entries ...FS) FS {
result := dir{
name: name,
entries: map[string]FS{},
}
for _, entry := range entries {
result.entries[entry.Name()] = entry
}
return result
}
func split(path string) (string, string) {
if !strings.HasPrefix(path, "/") {
panic(path)
}
comps := strings.SplitN(path, "/", 3)
if len(comps) == 2 {
return comps[1], "/"
}
return comps[1], "/" + comps[2]
}
func (mockInode) Size() int64 { return 0 }
func (mockInode) Mode() os.FileMode { return 0 }
func (mockInode) ModTime() time.Time { return time.Now() }
func (mockInode) Sys() interface{} { return nil }
func (p dir) Name() string { return p.name }
func (p dir) IsDir() bool { return true }
func (p dir) ReadDir(path string) ([]os.FileInfo, error) {
if path == "/" {
result := []os.FileInfo{}
for _, v := range p.entries {
result = append(result, v)
}
return result, nil
}
head, tail := split(path)
fs, ok := p.entries[head]
if !ok {
return nil, fmt.Errorf("Not found: %s", path)
}
return fs.ReadDir(tail)
}
func (p dir) ReadFile(path string) ([]byte, error) {
if path == "/" {
return nil, fmt.Errorf("I'm a directory!")
}
head, tail := split(path)
fs, ok := p.entries[head]
if !ok {
return nil, fmt.Errorf("Not found: %s", path)
}
return fs.ReadFile(tail)
}
func (p dir) Lstat(path string, stat *syscall.Stat_t) error {
if path == "/" {
return nil
}
head, tail := split(path)
fs, ok := p.entries[head]
if !ok {
return fmt.Errorf("Not found: %s", path)
}
return fs.Lstat(tail, stat)
}
func (p dir) Stat(path string, stat *syscall.Stat_t) error {
if path == "/" {
return nil
}
head, tail := split(path)
fs, ok := p.entries[head]
if !ok {
return fmt.Errorf("Not found: %s", path)
}
return fs.Stat(tail, stat)
}
func (p dir) Open(path string) (io.ReadWriteCloser, error) {
if path == "/" {
return nil, fmt.Errorf("I'm a directory!")
}
head, tail := split(path)
fs, ok := p.entries[head]
if !ok {
return nil, fmt.Errorf("Not found: %s", path)
}
return fs.Open(tail)
}
// Name implements os.FileInfo
func (p File) Name() string { return p.FName }
// IsDir implements os.FileInfo
func (p File) IsDir() bool { return false }
// ReadDir implements FS
func (p File) ReadDir(path string) ([]os.FileInfo, error) {
return nil, fmt.Errorf("I'm a file!")
}
// ReadFile implements FS
func (p File) ReadFile(path string) ([]byte, error) {
if path != "/" {
return nil, fmt.Errorf("I'm a file!")
}
return []byte(p.FContents), nil
}
// Lstat implements FS
func (p File) Lstat(path string, stat *syscall.Stat_t) error {
if path != "/" {
return fmt.Errorf("I'm a file!")
}
*stat = p.FStat
return nil
}
// Stat implements FS
func (p File) Stat(path string, stat *syscall.Stat_t) error {
if path != "/" {
return fmt.Errorf("I'm a file!")
}
*stat = p.FStat
return nil
}
// Open implements FS
func (p File) Open(path string) (io.ReadWriteCloser, error) {
if path != "/" {
return nil, fmt.Errorf("I'm a file!")
}
return struct {
io.ReadWriter
io.Closer
}{
bytes.NewBuffer([]byte(p.FContents)),
ioutil.NopCloser(nil),
}, nil
}

View File

@@ -1,20 +0,0 @@
.PHONY: all build buildall test install bench
all: test build buildall install
build:
go build
go vet
golint .
buildall:
GOOS=darwin go build
GOOS=linux go build
test:
go test
install:
go install
bench:
go test -bench .

View File

@@ -1,64 +0,0 @@
Go module to list all TCP connections, with an option to try to find the owning PID and processname.
Works by reading /proc directly on Linux, and by executing `netstat` and `lsof -i` on Darwin.
Works for IPv4 and IPv6 TCP connections. Only established connections are listed; ports where something is only listening or TIME_WAITs are skipped.
If you want to find all processes you'll need to run this as root.
Status:
-------
Tested on Linux and Darwin (10.9).
Install:
--------
`go install`
Usage:
------
Only list the connections:
```
cs, err := procspy.Connections(false)
for c := cs.Next(); c != nil; c = cs.Next() {
...
}
```
List the connections and try to find the owning process:
```
cs, err := procspy.Connections(true)
for c := cs.Next(); c != nil; c = cs.Next() {
...
}
```
(See ./example\_test.go)
``` go
package main
import (
"fmt"
"github.com/weaveworks/procspy"
)
func main() {
lookupProcesses := true
cs, err := procspy.Connections(lookupProcesses)
if err != nil {
panic(err)
}
fmt.Printf("TCP Connections:\n")
for c := cs.Next(); c != nil; c = cs.Next() {
fmt.Printf(" - %v\n", c)
}
}
```

View File

@@ -1,20 +0,0 @@
package procspy_test
import (
"fmt"
"github.com/weaveworks/procspy"
)
func Example() {
lookupProcesses := true
cs, err := procspy.Connections(lookupProcesses)
if err != nil {
panic(err)
}
fmt.Printf("TCP Connections:\n")
for c := cs.Next(); c != nil; c = cs.Next() {
fmt.Printf(" - %v\n", c)
}
}

View File

@@ -1,18 +0,0 @@
package main
import (
"fmt"
"github.com/weaveworks/procspy"
)
func main() {
cs, err := procspy.Connections(true)
if err != nil {
panic(err)
}
fmt.Printf("TCP Connections:\n")
for c := cs.Next(); c != nil; c = cs.Next() {
fmt.Printf(" - %+v\n", c)
}
}

8
vendor/manifest vendored
View File

@@ -723,12 +723,6 @@
"branch": "master",
"path": "/odp"
},
{
"importpath": "github.com/weaveworks/procspy",
"repository": "https://github.com/weaveworks/procspy",
"revision": "cb970aa190c374d1e47711dbffb3c2c6e9ef0dd1",
"branch": "master"
},
{
"importpath": "github.com/weaveworks/weave/common",
"repository": "https://github.com/weaveworks/weave",
@@ -931,4 +925,4 @@
"branch": "master"
}
]
}
}