83 Commits

Author SHA1 Message Date
Dominic Breuker
9c63e5d6c5 prepare README.md for version 1.2.0 release 2019-08-22 20:32:18 +02:00
Dominic Breuker
c136be2f46 Merge pull request #8 from DominicBreuker/version-tag
Version logging
2019-08-22 20:27:41 +02:00
Dominic Breuker
831cac3196 add version logging to startup log messages for better troubleshooting 2019-08-22 17:56:12 +02:00
Dominic Breuker
3bd4885e22 change banner 2019-08-21 23:43:00 +02:00
Dominic Breuker
6f434e1d5b add kill switch for errno 22 error to shut down if the error does not go away 2019-08-21 23:42:39 +02:00
Dominic Breuker
4c0edc540c Merge pull request #7 from DominicBreuker/fix-read-syscall
Fix errno 22 errors
2019-08-11 23:12:40 +02:00
Dominic Breuker
0e4d11951b split off image version for docker image check 2019-08-11 23:09:44 +02:00
Dominic Breuker
8264b725de improve read syscall reliability by inreasing buffer size and handling old kernel errors 2019-08-11 23:01:53 +02:00
Dominic Breuker
0c973089dd Merge pull request #6 from socketz/patch-1
Fixed Makefile
2019-08-03 23:29:50 +02:00
socketz
d2b84b9415 Fixed Makefile
Removed line was triggering an error
2019-07-31 23:07:52 +02:00
Dominic Breuker
af4e8ff857 Merge pull request #4 from DominicBreuker/small-improvements
Small improvements
2019-04-25 08:46:42 +02:00
Dominic Breuker
21a0666ff1 enable coloring by UIDs 2019-04-24 22:30:14 +02:00
Dominic Breuker
d1b6518db5 turn process monitoring events into structured objects 2019-04-24 22:08:59 +02:00
Dominic Breuker
8a1838faee introduce --debug flag to hide excessive error messages by default but allow displaying them 2019-04-24 21:42:18 +02:00
Dominic Breuker
7d9d32412b bump go version to 1.12 2019-04-24 21:35:26 +02:00
Dominic Breuker
59960f9f37 integrate docker image builds into make targets 2019-04-24 21:31:43 +02:00
Dominic Breuker
f8b2492730 solve merge conflicts 2019-04-24 15:44:51 +02:00
Dominic Breuker
d2c8362729 add download links to readme 2018-06-09 22:51:00 +02:00
Dominic Breuker
aba533f86e switch to debian for build image to link against libc 2018-04-05 09:01:27 +02:00
Dominic Breuker
986cafff6f add build command for small binaries 2018-04-05 08:47:43 +02:00
Dominic Breuker
ab8dfc2252 delete the binaries - why on earth did I put them here in the first place... 2018-04-05 08:45:28 +02:00
Dominic Breuker
d4e20c3629 bundle chans to struct 2018-03-29 08:51:03 +02:00
Dominic Breuker
84db6dd806 add config to enable/disable colored output 2018-03-29 08:43:13 +02:00
Dominic Breuker
093b9ec69c update readme to include new flag -i 2018-03-28 09:39:50 +02:00
Dominic Breuker
8e61b2bd9d adjust main test to account for new config format 2018-03-28 09:06:21 +02:00
Dominic Breuker
056c91801d add flag to configure scanning interval 2018-03-28 09:01:25 +02:00
Dominic Breuker
1e67dc332b use factory method in walker test 2018-03-16 09:40:13 +01:00
Dominic Breuker
72bbfac3e0 add tests for main method 2018-03-16 09:33:22 +01:00
Dominic Breuker
3eed2c29aa add tests for main pspy methods 2018-03-15 09:22:22 +01:00
Dominic Breuker
fe0300a67c further extend inotify test 2018-03-14 09:21:14 +01:00
Dominic Breuker
f357059f36 extend inotify test 2018-03-14 09:12:45 +01:00
Dominic Breuker
1f52ae340c add tests and remove unused method in inotify 2018-03-14 08:49:54 +01:00
Dominic Breuker
fb7dff2d13 refactor start method 2018-03-12 08:58:01 +01:00
Dominic Breuker
8440e46afc refactor event parsing 2018-03-12 08:58:01 +01:00
Dominic Breuker
723fb79e06 refactor walker 2018-03-12 08:58:01 +01:00
Dominic Breuker
53bb0c2566 refactor again 2018-03-12 08:58:01 +01:00
Dominic Breuker
b7c5e0b984 refactor complex code into smaller pieces 2018-03-12 08:58:01 +01:00
Dominic Breuker
d56eca305e add badges 2018-03-12 08:58:01 +01:00
Dominic Breuker
8fea22e762 copy .git folder explicity 2018-03-12 08:58:01 +01:00
Dominic Breuker
5a2d99fafb fix circle again 2018-03-12 08:58:01 +01:00
Dominic Breuker
96fad366d0 fix circle 2018-03-12 08:58:01 +01:00
Dominic Breuker
87639a4cd2 try machine build 2018-03-12 08:58:01 +01:00
Dominic Breuker
5ac0573e41 add env for test reporter 2018-03-12 08:58:01 +01:00
Dominic Breuker
00e2783e2c put volume mount into correct docker statement 2018-03-12 08:58:01 +01:00
Dominic Breuker
42c4561c22 try code coverage reporting on circle 2018-03-12 08:58:01 +01:00
Dominic Breuker
67590d8216 fix stupid mistake 2018-03-12 08:58:01 +01:00
Dominic Breuker
0e5d2ed39e move unit tests from image build to contrainer run 2018-03-12 08:58:01 +01:00
Dominic Breuker
f4b2fdf6b6 skip tests just to see if e2e test would work 2018-03-12 08:58:01 +01:00
Dominic Breuker
8b7ed9c586 fix circle config 2018-03-12 08:58:01 +01:00
Dominic Breuker
56cfc0555b try docker based build 2018-03-12 08:58:01 +01:00
Dominic Breuker
2d2b523bb6 try if circleci works with inotify 2018-03-12 08:58:01 +01:00
Dominic Breuker
5cdc39457a try native test run 2018-03-12 08:58:01 +01:00
Dominic Breuker
3c755caea7 try travis with more recent docker version 2018-03-12 08:58:01 +01:00
Dominic Breuker
d2996b5229 make error more clear 2018-03-12 08:58:01 +01:00
Dominic Breuker
ad8d51f4db trigger travis 2018-03-12 08:58:01 +01:00
Dominic Breuker
eb2fe0c668 testing travis 2018-03-12 08:58:01 +01:00
Dominic Breuker
a6d948819f refactor process refresh 2018-03-12 08:58:01 +01:00
Dominic Breuker
65ec5b1202 refactor get pid 2018-03-12 08:58:01 +01:00
Dominic Breuker
d3c7681096 add specs for logging and proc scanner 2018-03-12 08:58:01 +01:00
Dominic Breuker
572ce2ef3e add specs for process scanner 2018-03-12 08:58:01 +01:00
Dominic Breuker
644d65be7b refactors psscanner 2018-03-12 08:58:01 +01:00
Dominic Breuker
9670b85f43 add tests for fswatcher 2018-03-12 08:58:01 +01:00
Dominic Breuker
1deb4838a5 add tests for fswatcher package 2018-03-12 08:58:01 +01:00
Dominic Breuker
94a12cf031 refactor inotify implementation 2018-03-12 08:58:01 +01:00
Dominic Breuker
2750defb63 try some more tsting 2018-03-12 08:58:01 +01:00
Dominic Breuker
d1c18d901a restructure inotify package and add some tests 2018-03-12 08:58:01 +01:00
Dominic Breuker
f5ca2dad75 experiment with some tests 2018-03-12 08:58:01 +01:00
Dominic Breuker
cb48cc1b37 start big refactoring 2018-03-12 08:58:01 +01:00
Dominic Breuker
95150e5e12 Create LICENSE 2018-02-19 08:16:37 +01:00
Dominic Breuker
628e20cd56 add badges for code quality 2018-02-18 22:42:22 +01:00
Dominic Breuker
01521a19cc update readme 2018-02-18 22:30:33 +01:00
Dominic Breuker
693bc1ef17 refactor readme 2018-02-18 22:29:07 +01:00
Dominic Breuker
773f370c35 build proper dockerized example 2018-02-18 14:03:59 +01:00
Dominic Breuker
6b1f75f4d8 add link to wikimedia 2018-02-13 22:55:50 +01:00
Dominic Breuker
c469335009 update readme 2018-02-13 22:54:25 +01:00
Dominic Breuker
c191b4b7f4 rename image to demo 2018-02-13 22:45:35 +01:00
Dominic Breuker
167782b25f use full gif 2018-02-13 22:42:40 +01:00
Dominic Breuker
f7d0089b1e add a readme and update binaries 2018-02-13 22:39:45 +01:00
Dominic Breuker
4495ae7c96 pass through arguments from command line 2018-02-13 21:41:41 +01:00
Dominic Breuker
c92dd9992f switch to dep and integrate cobra 2018-02-13 10:00:38 +01:00
Dominic Breuker
56f706b7e2 improve console output 2018-02-12 23:39:04 +01:00
Dominic Breuker
9bc66835a6 add some hacky experiments for inotify event parsing 2018-02-11 22:15:10 +01:00
Dominic Breuker
38c5d42bb4 integrate inotify syscalls 2018-02-09 09:50:31 +01:00
16 changed files with 222 additions and 126 deletions

View File

@@ -9,19 +9,18 @@ DEV_DOCKERFILE = $(PROJECT_DIR)/docker/Dockerfile.development
TEST_IMAGE = local/pspy-testing:latest
TEST_DOCKERFILE = $(PROJECT_DIR)/docker/Dockerfile.testing
VERSION = `git describe --tags --always || echo "unknown"`
BUILD_SHA = `git rev-parse HEAD || echo "unknown"`
# Run unit test and integration test inside container
test:
docker build -f $(TEST_DOCKERFILE) -t $(TEST_IMAGE) .
docker run -it --rm $(TEST_IMAGE)
# Build Docker image for development
# pspy has to run on Linux - use this if you develop on another OS
dev-build:
docker build -f $(DEV_DOCKERFILE) -t $(DEV_IMAGE) .
# Drops you into a shell in the development container and mounts the source code
# You can edit to source on your host, then run go commans (e.g., `go test ./...`) inside the container
dev:
sh -c "if ! docker image ls | grep '$(DEV_IMAGE)' | cut -d ':' -f1; then echo 'building dev image'; docker build -f $(DEV_DOCKERFILE) -t $(DEV_IMAGE) .; fi"
docker run -it \
--rm \
-v $(PROJECT_DIR):/go/src/github.com/dominicbreuker/pspy \
@@ -37,14 +36,13 @@ example:
docker run -it --rm $(EXAMPLE_IMAGE)
build-build-image:
docker build -f $(BUILD_DOCKERFILE) -t $(BUILD_IMAGE) .
# Build different binaries
# builds binaries for both 32bit and 64bit systems
# builds one set of static binaries that should work on any system without dependencies, but are huge
# builds another set of binaries that are as small as possible, but may not work
build:
# sh -c "if ! docker image ls | grep '$(BUILD_IMAGE)' | cut -d ':' -f1; then echo 'building build image'; docker build -f $(BUILD_DOCKERFILE) -t $(BUILD_IMAGE) .; fi"
mkdir -p $(PROJECT_DIR)/bin
docker run -it \
--rm \
@@ -53,7 +51,7 @@ build:
--env CGO_ENABLED=0 \
--env GOOS=linux \
--env GOARCH=386 \
$(BUILD_IMAGE) /bin/sh -c "go build -a -ldflags '-extldflags \"-static\"' -o pspy/bin/pspy32 pspy/main.go"
$(BUILD_IMAGE) /bin/sh -c "go build -a -ldflags '-s -w -X main.version=${VERSION} -X main.commit=${BUILD_SHA} -extldflags \"-static\"' -o pspy/bin/pspy32 pspy/main.go"
docker run -it \
--rm \
-v $(PROJECT_DIR):/go/src/github.com/dominicbreuker/pspy \
@@ -61,18 +59,18 @@ build:
--env CGO_ENABLED=0 \
--env GOOS=linux \
--env GOARCH=amd64 \
$(BUILD_IMAGE) /bin/sh -c "go build -a -ldflags '-extldflags \"-static\"' -o pspy/bin/pspy64 pspy/main.go"
$(BUILD_IMAGE) /bin/sh -c "go build -a -ldflags '-s -w -X main.version=${VERSION} -X main.commit=${BUILD_SHA} -extldflags \"-static\"' -o pspy/bin/pspy64 pspy/main.go"
docker run -it \
--rm \
-v $(PROJECT_DIR):/go/src/github.com/dominicbreuker/pspy \
-w "/go/src/github.com/dominicbreuker" \
--env GOOS=linux \
--env GOARCH=386 \
$(BUILD_IMAGE) /bin/sh -c "go build -ldflags '-w -s' -o pspy/bin/pspy32s pspy/main.go && upx pspy/bin/pspy32s"
$(BUILD_IMAGE) /bin/sh -c "go build -ldflags '-w -s -X main.version=${VERSION} -X main.commit=${BUILD_SHA}' -o pspy/bin/pspy32s pspy/main.go && upx pspy/bin/pspy32s"
docker run -it \
--rm \
-v $(PROJECT_DIR):/go/src/github.com/dominicbreuker/pspy \
-w "/go/src/github.com/dominicbreuker" \
--env GOOS=linux \
--env GOARCH=amd64 \
$(BUILD_IMAGE) /bin/sh -c "go build -ldflags '-w -s' -o pspy/bin/pspy64s pspy/main.go && upx pspy/bin/pspy64s"
$(BUILD_IMAGE) /bin/sh -c "go build -ldflags '-w -s -X main.version=${VERSION} -X main.commit=${BUILD_SHA}' -o pspy/bin/pspy64s pspy/main.go && upx pspy/bin/pspy64s"

View File

@@ -1,6 +1,6 @@
<img src="images/logo.svg" align="left" />
# pspy - unprivileged linux process snooping
# pspy - unprivileged Linux process snooping
[![Go Report Card](https://goreportcard.com/badge/github.com/DominicBreuker/pspy)](https://goreportcard.com/report/github.com/DominicBreuker/pspy)
[![Maintainability](https://api.codeclimate.com/v1/badges/23328b2549a76aa11dd5/maintainability)](https://codeclimate.com/github/DominicBreuker/pspy/maintainability)
@@ -12,21 +12,27 @@ It allows you to see commands run by other users, cron jobs, etc. as they execut
Great for enumeration of Linux systems in CTFs.
Also great to demonstrate your colleagues why passing secrets as arguments on the command line is a bad idea.
The tool gathers it's info from procfs scans.
The tool gathers the info from procfs scans.
Inotify watchers placed on selected parts of the file system trigger these scans to catch short-lived processes.
## Getting started
Get the tool onto the Linux machine you want to inspect.
First get the binaries.
### Download
Get the tool onto the Linux machine you want to inspect.
First get the binaries. Download the released binaries here:
- 32 bit big, static version: `pspy32` [download](https://github.com/DominicBreuker/pspy/releases/download/v1.2.0/pspy32)
- 64 bit big, static version: `pspy64` [download](https://github.com/DominicBreuker/pspy/releases/download/v1.2.0/pspy64)
- 32 bit small version: `pspy32s` [download](https://github.com/DominicBreuker/pspy/releases/download/v1.2.0/pspy32s)
- 64 bit small version: `pspy64s` [download](https://github.com/DominicBreuker/pspy/releases/download/v1.2.0/pspy64s)
You can build them yourself by running `make build-build-image` to build a docker image used in `make build` to build four binaries:
- 32 bit big, static version: `pspy32`
- 64 bit big, static version: `pspy64`
- 32 bit small version: `pspy32s`
- 64 bit small version: `pspy64s`
The statically compiled files should work on any Linux system but are quite huge (~4MB).
If size is an issue, try the smaller versions which depend on libc and are compressed with UPX (<1MB).
If size is an issue, try the smaller versions which depend on libc and are compressed with UPX (~1MB).
### Build
Either use Go installed on your system or run the Docker-based build process which ran to create the release.
For the latter, ensure Docker is installed, and then run `make build-build-image` to build a Docker image, followed by `make build` to build the binaries with it.
You can run `pspy --help` to learn about the flags and their meaning.
The summary is as follows:
@@ -35,7 +41,8 @@ The summary is as follows:
- -r: list of directories to watch with Inotify. pspy will watch all subdirectories recursively (by default, watches /usr, /tmp, /etc, /home, /var, and /opt).
- -d: list of directories to watch with Inotify. pspy will watch these directories only, not the subdirectories (empty by default).
- -i: interval in milliseconds between procfs scans. pspy scans regularly for new processes regardless of Inotify events, just in case some events are not received.
- -c: print events in different colors. Red for new processes, green for new Inotify events.
- -c: print commands in different colors. File system events are not colored anymore, commands have different colors based on process UID.
- --debug: prints verbose error messages which are otherwise hidden.
The default settings should be fine for most applications.
Watching files inside `/usr` is most important since many tools will access libraries inside it.

View File

@@ -5,7 +5,6 @@ import (
"log"
"os"
"os/signal"
"strings"
"syscall"
"time"
@@ -17,15 +16,20 @@ import (
"github.com/spf13/cobra"
)
var bannerLines = []string{
" _____ _____ _______ __",
" | __ \\ / ____| __ \\ \\ / /",
" | |__) | (___ | |__) \\ \\_/ / ",
" | ___/ \\___ \\| ___/ \\ / ",
" | | ____) | | | | ",
" |_| |_____/|_| |_| ",
helpText,
}
var banner = `
██▓███ ██████ ██▓███ ▓██ ██▓
▓██░ ██▒▒██ ▒ ▓██░ ██▒▒██ ██▒
▓██░ ██▓▒░ ▓██▄ ▓██░ ██▓▒ ▒██ ██░
▒██▄█▓▒ ▒ ▒ ██▒▒██▄█▓▒ ▒ ░ ▐██▓░
▒██▒ ░ ░▒██████▒▒▒██▒ ░ ░ ░ ██▒▓░
▒▓▒░ ░ ░▒ ▒▓▒ ▒ ░▒▓▒░ ░ ░ ██▒▒▒
░▒ ░ ░ ░▒ ░ ░░▒ ░ ▓██ ░▒░
░░ ░ ░ ░ ░░ ▒ ▒ ░░
░ ░ ░
░ ░
`
var helpText = `
pspy monitors the system for file system events and new processes.
@@ -33,11 +37,9 @@ It prints these envents to the console.
File system events are monitored with inotify.
Processes are monitored by scanning /proc, using file system events as triggers.
pspy does not require root permissions do operate.
Check our https://github.com/dominicbreuker/pspy for more information.
Check out https://github.com/dominicbreuker/pspy for more information.
`
var banner = strings.Join(bannerLines, "\n")
var rootCmd = &cobra.Command{
Use: "pspy",
Short: "pspy can watch your system for new processes and file system events",
@@ -58,6 +60,7 @@ var defaultRDirs = []string{
var defaultDirs = []string{}
var triggerInterval int
var colored bool
var debug bool
func init() {
rootCmd.PersistentFlags().BoolVarP(&logPS, "procevents", "p", true, "print new processes to stdout")
@@ -66,12 +69,15 @@ func init() {
rootCmd.PersistentFlags().StringArrayVarP(&dirs, "dirs", "d", defaultDirs, "watch these dirs")
rootCmd.PersistentFlags().IntVarP(&triggerInterval, "interval", "i", 100, "scan every 'interval' milliseconds for new processes")
rootCmd.PersistentFlags().BoolVarP(&colored, "color", "c", true, "color the printed events")
rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "", false, "print detailed error messages")
log.SetOutput(os.Stdout)
}
func root(cmd *cobra.Command, args []string) {
logger := logging.NewLogger()
logger := logging.NewLogger(debug)
logger.Infof("%s", banner)
cfg := &config.Config{
RDirs: rDirs,

View File

@@ -1,3 +1,3 @@
FROM golang:1.10-stretch
FROM golang:1.12-stretch
RUN apt-get update && apt-get install -y upx && rm -rf /var/lib/apt/lists/*

View File

@@ -1,4 +1,4 @@
FROM golang:1.10-stretch
FROM golang:1.12-stretch
RUN apt-get update && apt-get -y install cron python3 sudo procps

View File

@@ -1,4 +1,4 @@
FROM golang:1.10-stretch
FROM golang:1.12-stretch
RUN apt-get update && apt-get -y install cron python3 sudo procps

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
"unsafe"
@@ -17,7 +18,8 @@ const maximumWatchersFile = "/proc/sys/fs/inotify/max_user_watches"
// set to -1 if the number cannot be determined
var MaxWatchers int = -1
const EventSize int = unix.SizeofInotifyEvent
// sizeof(struct inotify_event) + NAME_MAX + 1
const EventSize int = unix.SizeofInotifyEvent + 255 + 1
func init() {
mw, err := getMaxWatchers()
@@ -69,9 +71,20 @@ func (i *Inotify) Watch(dir string) error {
return nil
}
var errno22Counter = 0
func (i *Inotify) Read(buf []byte) (int, error) {
n, errno := unix.Read(i.FD, buf)
if n < 0 {
if n < 1 {
if errno.Error() == "invalid argument" {
errno22Counter += 1
if errno22Counter > 20 {
fmt.Printf("Unrecoverable inotify error (%s, errno %d). Exiting program...\n", errno, errno)
os.Exit(22)
}
} else {
errno22Counter = 0
}
return n, fmt.Errorf("reading from inotify fd %d: errno: %d", i.FD, errno)
}
return n, nil

View File

@@ -2,27 +2,35 @@ package logging
import (
"fmt"
"hash/fnv"
"log"
"os"
"strconv"
)
const (
ColorNone = iota
ColorRed
ColorGreen
ColorYellow
ColorBlue
ColorPurple
ColorTeal
)
type Logger struct {
infoLogger *log.Logger
errorLogger *log.Logger
eventLogger *log.Logger
debug bool
}
func NewLogger() *Logger {
func NewLogger(debug bool) *Logger {
return &Logger{
infoLogger: log.New(os.Stdout, "", 0),
errorLogger: log.New(os.Stderr, "", 0),
eventLogger: log.New(os.Stdout, "", log.Ldate|log.Ltime),
debug: debug,
}
}
@@ -32,21 +40,24 @@ func (l *Logger) Infof(format string, v ...interface{}) {
}
// Errorf writes an error message to stderr
func (l *Logger) Errorf(format string, v ...interface{}) {
func (l *Logger) Errorf(debug bool, format string, v ...interface{}) {
if l.debug == debug {
l.errorLogger.Printf(format, v...)
}
}
// Eventf writes an event with timestamp to stdout
func (l *Logger) Eventf(color int, format string, v ...interface{}) {
msg := fmt.Sprintf(format, v...)
switch color {
case ColorRed:
msg = fmt.Sprintf("\x1b[31;1m%s\x1b[0m", msg)
case ColorGreen:
msg = fmt.Sprintf("\x1b[32;1m%s\x1b[0m", msg)
default:
if color != ColorNone {
msg = fmt.Sprintf("\x1b[%d;1m%s\x1b[0m", 30+color, msg)
}
l.eventLogger.Printf("%s", msg)
}
func GetColorByUID(uid int) int {
h := fnv.New32a()
h.Write([]byte(strconv.Itoa(uid)))
return (int(h.Sum32()) % (ColorTeal)) + 1
}

View File

@@ -13,7 +13,7 @@ const ansiPattern = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\
var ansiMatcher = regexp.MustCompile(ansiPattern)
var l = NewLogger()
var l = NewLogger(true)
var logTests = []struct {
logger *log.Logger
@@ -23,8 +23,8 @@ var logTests = []struct {
}{
{l.infoLogger, func() { l.Infof("Info message no. %d", 1) }, "Info message no. 1\n", nil},
{l.infoLogger, func() { l.Infof("Info message no. %d with a string %s\n", 2, "appended to it") }, "Info message no. 2 with a string appended to it\n", nil},
{l.errorLogger, func() { l.Errorf("Error message") }, "Error message\n", nil},
{l.errorLogger, func() { l.Errorf("Error message\n") }, "Error message\n", nil},
{l.errorLogger, func() { l.Errorf(true, "Error message") }, "Error message\n", nil},
{l.errorLogger, func() { l.Errorf(true, "Error message\n") }, "Error message\n", nil},
{l.eventLogger, func() { l.Eventf(ColorNone, "Event message") }, dateFormatPattern + " Event message\n", nil},
{l.eventLogger, func() { l.Eventf(ColorRed, "Event message") }, dateFormatPattern + " Event message\n", [][]byte{[]byte("\x1b[31;1m"), []byte("\x1b[0m")}},
{l.eventLogger, func() { l.Eventf(ColorGreen, "Event message") }, dateFormatPattern + " Event message\n", [][]byte{[]byte("\x1b[32;1m"), []byte("\x1b[0m")}},
@@ -33,6 +33,7 @@ var logTests = []struct {
func TestLogging(t *testing.T) {
for i, tt := range logTests {
actual := captureOutput(tt.logger, tt.test)
log.Printf("OUT: %s", actual)
// check colors and remove afterwards
colors := ansiMatcher.FindAll(actual, 2)
@@ -55,3 +56,46 @@ func captureOutput(logger *log.Logger, f func()) []byte {
f()
return buf.Bytes()
}
func TestGetColorByUID(t *testing.T) {
tests := []struct {
uid int
color int
}{
{uid: 0, color: 4},
{uid: 1, color: 5},
{uid: 2, color: 2},
{uid: 3, color: 3},
{uid: 99999999999, color: 5},
}
for _, tt := range tests {
color := GetColorByUID(tt.uid)
if color != tt.color {
t.Errorf("GetColorByUID(%d)=%d but want %d", tt.uid, color, tt.color)
}
}
minColor := 9999999
maxColor := -9999999
for i := 0; i < 1000; i++ {
color := GetColorByUID(i)
if color < 1 || color > ColorTeal {
t.Fatalf("GetColorByUID(%d)=%d but this is out of range [%d, %d]", i, color, ColorRed, ColorTeal)
}
if color < minColor {
minColor = color
}
if color > maxColor {
maxColor = color
}
}
if minColor != 1 {
t.Errorf("GetColorByUID returned minimum color %d, not %d, on 1000 trials, which is extremely unlikely", minColor, 1)
}
if maxColor != ColorTeal {
t.Errorf("GetColorByUID returned maximum color %d, not %d, on 1000 trials, which is extremely unlikely", maxColor, ColorTeal)
}
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/dominicbreuker/pspy/internal/config"
"github.com/dominicbreuker/pspy/internal/logging"
"github.com/dominicbreuker/pspy/internal/psscanner"
)
type Bindings struct {
@@ -16,7 +17,7 @@ type Bindings struct {
type Logger interface {
Infof(format string, v ...interface{})
Errorf(format string, v ...interface{})
Errorf(debug bool, format string, v ...interface{})
Eventf(color int, format string, v ...interface{})
}
@@ -26,13 +27,13 @@ type FSWatcher interface {
}
type PSScanner interface {
Run(triggerCh chan struct{}) (chan string, chan error)
Run(triggerCh chan struct{}) (chan psscanner.PSEvent, chan error)
}
type chans struct {
sigCh chan os.Signal
fsEventCh chan string
psEventCh chan string
psEventCh chan psscanner.PSEvent
}
func Start(cfg *config.Config, b *Bindings, sigCh chan os.Signal) chan struct{} {
@@ -56,7 +57,7 @@ func Start(cfg *config.Config, b *Bindings, sigCh chan os.Signal) chan struct{}
func printOutput(cfg *config.Config, b *Bindings, chans *chans) chan struct{} {
exit := make(chan struct{})
fsEventColor, psEventColor := getColors(cfg.Colored)
// fsEventColor, psEventColor := getColors(cfg.Colored)
go func() {
for {
@@ -66,11 +67,15 @@ func printOutput(cfg *config.Config, b *Bindings, chans *chans) chan struct{} {
exit <- struct{}{}
case fe := <-chans.fsEventCh:
if cfg.LogFS {
b.Logger.Eventf(fsEventColor, "FS: %+v", fe)
b.Logger.Eventf(logging.ColorNone, "FS: %+v", fe)
}
case pe := <-chans.psEventCh:
if cfg.LogPS {
b.Logger.Eventf(psEventColor, "CMD: %+v", pe)
color := logging.ColorNone
if cfg.Colored {
color = logging.GetColorByUID(pe.UID)
}
b.Logger.Eventf(color, "CMD: %+v", pe)
}
}
}
@@ -78,17 +83,6 @@ func printOutput(cfg *config.Config, b *Bindings, chans *chans) chan struct{} {
return exit
}
func getColors(colored bool) (fsEventColor int, psEventColor int) {
if colored {
fsEventColor = logging.ColorGreen
psEventColor = logging.ColorRed
} else {
fsEventColor = logging.ColorNone
psEventColor = logging.ColorNone
}
return
}
func initFSW(fsw FSWatcher, rdirs, dirs []string, logger Logger) {
errCh, doneCh := fsw.Init(rdirs, dirs)
for {
@@ -96,7 +90,7 @@ func initFSW(fsw FSWatcher, rdirs, dirs []string, logger Logger) {
case <-doneCh:
return
case err := <-errCh:
logger.Errorf("initializing fs watcher: %v", err)
logger.Errorf(true, "initializing fs watcher: %v", err)
}
}
}
@@ -112,7 +106,7 @@ func startFSW(fsw FSWatcher, logger Logger, drainFor time.Duration) (triggerCh c
return triggerCh, fsEventCh
}
func startPSS(pss PSScanner, logger Logger, triggerCh chan struct{}) (psEventCh chan string) {
func startPSS(pss PSScanner, logger Logger, triggerCh chan struct{}) (psEventCh chan psscanner.PSEvent) {
psEventCh, errCh := pss.Run(triggerCh)
go logErrors(errCh, logger)
return psEventCh
@@ -130,7 +124,7 @@ func triggerEvery(d time.Duration, triggerCh chan struct{}) {
func logErrors(errCh chan error, logger Logger) {
for {
err := <-errCh
logger.Errorf("ERROR: %v", err)
logger.Errorf(true, "ERROR: %v", err)
}
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/dominicbreuker/pspy/internal/config"
"github.com/dominicbreuker/pspy/internal/logging"
"github.com/dominicbreuker/pspy/internal/psscanner"
)
func TestInitFSW(t *testing.T) {
@@ -67,24 +68,6 @@ func TestStartPSS(t *testing.T) {
expectMessage(t, l.Error, "ERROR: error during refresh")
}
func TestGetColors(t *testing.T) {
tests := []struct {
colored bool
fsEventColor int
psEventColor int
}{
{colored: true, fsEventColor: logging.ColorGreen, psEventColor: logging.ColorRed},
{colored: false, fsEventColor: logging.ColorNone, psEventColor: logging.ColorNone},
}
for _, tt := range tests {
c1, c2 := getColors(tt.colored)
if c1 != tt.fsEventColor || c2 != tt.psEventColor {
t.Errorf("Got wrong colors when colored=%t: expected %d, %d but got %d, %d", tt.colored, tt.fsEventColor, tt.psEventColor, c1, c2)
}
}
}
func TestStart(t *testing.T) {
drainFor := 10 * time.Millisecond
triggerEvery := 999 * time.Second
@@ -112,7 +95,7 @@ func TestStart(t *testing.T) {
close(fsw.initDoneCh)
<-time.After(2 * drainFor)
fsw.runTriggerCh <- struct{}{}
pss.runEventCh <- "pss event"
pss.runEventCh <- psscanner.PSEvent{UID: 1000, PID: 12345, CMD: "pss event"}
pss.runErrCh <- errors.New("pss error")
fsw.runEventCh <- "fsw event"
fsw.runErrCh <- errors.New("fsw error")
@@ -125,9 +108,9 @@ func TestStart(t *testing.T) {
<-time.After(2 * drainFor)
expectMessage(t, l.Info, "done")
expectTrigger(t, pss.runTriggerCh) // pss receives triggers from fsw
expectMessage(t, l.Event, fmt.Sprintf("%d CMD: pss event", logging.ColorRed))
expectMessage(t, l.Event, fmt.Sprintf("%d CMD: UID=1000 PID=12345 | pss event", logging.ColorPurple))
expectMessage(t, l.Error, "ERROR: pss error")
expectMessage(t, l.Event, fmt.Sprintf("%d FS: fsw event", logging.ColorGreen))
expectMessage(t, l.Event, fmt.Sprintf("%d FS: fsw event", logging.ColorNone))
expectMessage(t, l.Error, "ERROR: fsw error")
expectMessage(t, l.Info, "Exiting program... (interrupt)")
@@ -190,6 +173,7 @@ type mockLogger struct {
Info chan string
Error chan string
Event chan string
Debug bool
}
func newMockLogger() *mockLogger {
@@ -197,6 +181,7 @@ func newMockLogger() *mockLogger {
Info: make(chan string, 10),
Error: make(chan string, 10),
Event: make(chan string, 10),
Debug: true,
}
}
@@ -204,9 +189,11 @@ func (l *mockLogger) Infof(format string, v ...interface{}) {
l.Info <- fmt.Sprintf(format, v...)
}
func (l *mockLogger) Errorf(format string, v ...interface{}) {
func (l *mockLogger) Errorf(debug bool, format string, v ...interface{}) {
if l.Debug == debug {
l.Error <- fmt.Sprintf(format, v...)
}
}
func (l *mockLogger) Eventf(color int, format string, v ...interface{}) {
m := fmt.Sprintf(format, v...)
@@ -251,7 +238,7 @@ func (fsw *mockFSWatcher) Run() (chan struct{}, chan string, chan error) {
type mockPSScanner struct {
runTriggerCh chan struct{}
runEventCh chan string
runEventCh chan psscanner.PSEvent
runErrCh chan error
numRefreshes int
}
@@ -260,9 +247,9 @@ func newMockPSScanner() *mockPSScanner {
return &mockPSScanner{}
}
func (pss *mockPSScanner) Run(triggerCh chan struct{}) (chan string, chan error) {
func (pss *mockPSScanner) Run(triggerCh chan struct{}) (chan psscanner.PSEvent, chan error) {
pss.runTriggerCh = triggerCh
pss.runEventCh = make(chan string)
pss.runEventCh = make(chan psscanner.PSEvent)
pss.runErrCh = make(chan error)
go func() {

View File

@@ -25,7 +25,7 @@ var cmdLineReader = func(pid int) ([]byte, error) {
type procList map[int]string
func (pl procList) refresh(eventCh chan string) error {
func (pl procList) refresh(eventCh chan PSEvent) error {
pids, err := getPIDs()
if err != nil {
return err
@@ -74,16 +74,16 @@ func file2Pid(f os.FileInfo) (int, error) {
return pid, nil
}
func (pl procList) addPid(pid int, eventCh chan string) {
func (pl procList) addPid(pid int, eventCh chan PSEvent) {
cmd, err := getCmd(pid)
if err != nil {
cmd = "???" // process probably terminated
}
uid, err := getUID(pid)
if err != nil {
uid = "???"
uid = -1
}
eventCh <- fmt.Sprintf("UID=%-4s PID=%-6d | %s", uid, pid, cmd)
eventCh <- PSEvent{UID: uid, PID: pid, CMD: cmd}
pl[pid] = cmd
}
@@ -100,18 +100,26 @@ func getCmd(pid int) (string, error) {
return string(cmd), nil
}
func getUID(pid int) (string, error) {
func getUID(pid int) (int, error) {
stat, err := procStatusReader(pid)
if err != nil {
return "", err
return -1, err
}
lines := strings.Split(string(stat), "\n")
if len(lines) < 9 {
return "", fmt.Errorf("no uid information")
return -1, fmt.Errorf("no uid information")
}
uidL := strings.Split(lines[8], "\t")
if len(uidL) < 2 {
return "", fmt.Errorf("uid line read incomplete")
return -1, fmt.Errorf("uid line read incomplete")
}
return uidL[1], nil
uid, err := strconv.Atoi(uidL[1])
if err != nil {
return -1, fmt.Errorf("converting %s to int: %v", uidL[1], err)
}
return uid, nil
}

View File

@@ -136,14 +136,14 @@ func TestGetUID(t *testing.T) {
pid int
stat []byte
statErr error
uid string
uid int
err string
}{
{pid: 7, stat: completeStatus, statErr: nil, uid: "0", err: ""}, // can read normal stat
{pid: 7, stat: uidLineBroken, statErr: nil, uid: "", err: "uid line read incomplete"}, // errors on incomplete Uid line
{pid: 7, stat: notEnoughLines, statErr: nil, uid: "", err: "no uid information"}, // errors for insufficient lines
{pid: 7, stat: []byte(""), statErr: nil, uid: "", err: "no uid information"}, // errors for insufficient lines
{pid: 7, stat: []byte(""), statErr: errors.New("file-system-error"), uid: "", err: "file-system-error"}, // returns file system errors
{pid: 7, stat: completeStatus, statErr: nil, uid: 0, err: ""}, // can read normal stat
{pid: 7, stat: uidLineBroken, statErr: nil, uid: -1, err: "uid line read incomplete"}, // errors on incomplete Uid line
{pid: 7, stat: notEnoughLines, statErr: nil, uid: -1, err: "no uid information"}, // errors for insufficient lines
{pid: 7, stat: []byte(""), statErr: nil, uid: -1, err: "no uid information"}, // errors for insufficient lines
{pid: 7, stat: []byte(""), statErr: errors.New("file-system-error"), uid: -1, err: "file-system-error"}, // returns file system errors
}
for _, tt := range tests {
@@ -151,7 +151,7 @@ func TestGetUID(t *testing.T) {
uid, err := getUID(tt.pid)
if uid != tt.uid {
fmt.Printf("STAT: %s", tt.stat)
t.Errorf("Wrong uid returned: got %s but want %s", uid, tt.uid)
t.Errorf("Wrong uid returned: got %d but want %d", uid, tt.uid)
}
if (err != nil || tt.err != "") && fmt.Sprintf("%v", err) != tt.err {
t.Errorf("Wrong error returned: got %v but want %s", err, tt.err)
@@ -174,18 +174,18 @@ func mockProcStatusReader(stat []byte, err error) (restore func()) {
func TestRefresh(t *testing.T) {
tests := []struct {
eventCh chan string
eventCh chan PSEvent
pl procList
newPids []int
pidsAfter []int
events []string
}{
{eventCh: make(chan string), pl: procList{}, newPids: []int{1, 2, 3}, pidsAfter: []int{3, 2, 1}, events: []string{
{eventCh: make(chan PSEvent), pl: procList{}, newPids: []int{1, 2, 3}, pidsAfter: []int{3, 2, 1}, events: []string{
"UID=??? PID=3 | the-command",
"UID=??? PID=2 | the-command",
"UID=??? PID=1 | the-command",
}},
{eventCh: make(chan string), pl: procList{1: "pid-found-before"}, newPids: []int{1, 2, 3}, pidsAfter: []int{1, 3, 2}, events: []string{
{eventCh: make(chan PSEvent), pl: procList{1: "pid-found-before"}, newPids: []int{1, 2, 3}, pidsAfter: []int{1, 3, 2}, events: []string{
"UID=??? PID=3 | the-command",
"UID=??? PID=2 | the-command",
}}, // no events emitted for PIDs already known
@@ -200,7 +200,7 @@ func TestRefresh(t *testing.T) {
done := make(chan struct{})
go func() {
for e := range tt.eventCh {
events = append(events, e)
events = append(events, e.String())
}
done <- struct{}{}
}()

View File

@@ -1,13 +1,33 @@
package psscanner
import (
"fmt"
"strconv"
)
type PSScanner struct{}
type PSEvent struct {
UID int
PID int
CMD string
}
func (evt PSEvent) String() string {
uid := strconv.Itoa(evt.UID)
if evt.UID == -1 {
uid = "???"
}
return fmt.Sprintf("UID=%-4s PID=%-6d | %s", uid, evt.PID, evt.CMD)
}
func NewPSScanner() *PSScanner {
return &PSScanner{}
}
func (p *PSScanner) Run(triggerCh chan struct{}) (chan string, chan error) {
eventCh := make(chan string, 100)
func (p *PSScanner) Run(triggerCh chan struct{}) (chan PSEvent, chan error) {
eventCh := make(chan PSEvent, 100)
errCh := make(chan error)
pl := make(procList)

View File

@@ -48,7 +48,7 @@ func TestRun(t *testing.T) {
case <-time.After(timeout):
t.Errorf("did not receive event in time")
case e := <-eventCh:
if e != tt.events[i] {
if e.String() != tt.events[i] {
t.Errorf("Wrong event received: got '%s' but wanted '%s'", e, tt.events[i])
}
case err := <-errCh:

10
main.go
View File

@@ -1,7 +1,15 @@
package main
import "github.com/dominicbreuker/pspy/cmd"
import (
"fmt"
"github.com/dominicbreuker/pspy/cmd"
)
var version string
var commit string
func main() {
fmt.Printf("pspy - version: %s - Commit SHA: %s\n", version, commit)
cmd.Execute()
}