mirror of
https://github.com/bcicen/ctop.git
synced 2025-12-06 23:26:45 +08:00
Compare commits
272 Commits
v0.5.1
...
text-col-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68e4c32c1b | ||
|
|
a63f05b430 | ||
|
|
426dd2c985 | ||
|
|
0fb627a529 | ||
|
|
c8f74a47a1 | ||
|
|
41c04fefa2 | ||
|
|
5b2d180f60 | ||
|
|
2fdbb91f87 | ||
|
|
c0703db094 | ||
|
|
53ec5c911a | ||
|
|
c5038e2edd | ||
|
|
e1a52a314d | ||
|
|
bbecbc66b9 | ||
|
|
192d3eaa7a | ||
|
|
d34de844e0 | ||
|
|
a8e235beca | ||
|
|
ed194e8c04 | ||
|
|
09566a4043 | ||
|
|
f11a705b8b | ||
|
|
6fe6e7c316 | ||
|
|
9aa104fbc6 | ||
|
|
7c6b5c54dc | ||
|
|
ffb96f4e90 | ||
|
|
fc9bd9e5ca | ||
|
|
c7a8bfa26f | ||
|
|
6b79e5a370 | ||
|
|
54fc5ac5c6 | ||
|
|
eb8237cbb1 | ||
|
|
60875b179c | ||
|
|
15c5c31726 | ||
|
|
ea5968edce | ||
|
|
c0db41ebcb | ||
|
|
dc14c79edf | ||
|
|
0ca5235ae5 | ||
|
|
8427b0c81d | ||
|
|
9bcf2c2c7a | ||
|
|
03a0da3230 | ||
|
|
4d7d69d4cf | ||
|
|
fae9deb1d9 | ||
|
|
8027b990f8 | ||
|
|
c446fb0e11 | ||
|
|
4741b276e4 | ||
|
|
d60b16aad1 | ||
|
|
f704898212 | ||
|
|
1523cc80ca | ||
|
|
b16561dccb | ||
|
|
bf3b89a010 | ||
|
|
5585a22962 | ||
|
|
ca5d40b7cc | ||
|
|
50d1c29d57 | ||
|
|
22a5607012 | ||
|
|
6e60fc905e | ||
|
|
118b89240d | ||
|
|
ee25f80a9c | ||
|
|
416eb5c363 | ||
|
|
746da760fb | ||
|
|
cc6f706c4b | ||
|
|
1ca40bb7e1 | ||
|
|
918ccdbe39 | ||
|
|
8fcd14e097 | ||
|
|
9dd12103fc | ||
|
|
c38942c7ed | ||
|
|
d34b9c2bf6 | ||
|
|
a60861437f | ||
|
|
4b391e900c | ||
|
|
4460162380 | ||
|
|
d56cc9475a | ||
|
|
4584cf34f5 | ||
|
|
1ce07448ce | ||
|
|
c8e896e371 | ||
|
|
db2c832bd7 | ||
|
|
7fdcd7bbf1 | ||
|
|
923edb967e | ||
|
|
1271ce96e8 | ||
|
|
d8c7dd4c5c | ||
|
|
b7d81485f9 | ||
|
|
8946c4b03b | ||
|
|
331f50f03e | ||
|
|
4c4f041b40 | ||
|
|
c8ac331652 | ||
|
|
0a5a4c9062 | ||
|
|
98fcfe8b6f | ||
|
|
42f095cd85 | ||
|
|
73986d2732 | ||
|
|
c1d4615cc0 | ||
|
|
d187e8c623 | ||
|
|
b8c38d09ef | ||
|
|
d7384db373 | ||
|
|
1b441db189 | ||
|
|
0479d42e31 | ||
|
|
b401e7b17e | ||
|
|
9592de82a0 | ||
|
|
29fa8cf3e7 | ||
|
|
c49939f965 | ||
|
|
2f7bc2a172 | ||
|
|
7b4d4db049 | ||
|
|
70bd2ae3a3 | ||
|
|
665e8fdd06 | ||
|
|
101ddad692 | ||
|
|
ca35ef2aab | ||
|
|
d59c91a461 | ||
|
|
a39b7a3a3e | ||
|
|
77f5e6b735 | ||
|
|
3c83b7576b | ||
|
|
8a0bd3cf8a | ||
|
|
78caad2dbd | ||
|
|
8d8f1e72eb | ||
|
|
93556a1754 | ||
|
|
4d247f5272 | ||
|
|
db3d7e8927 | ||
|
|
efef345665 | ||
|
|
f158fa742f | ||
|
|
4d48245d7d | ||
|
|
6bee1b7f31 | ||
|
|
7118e45f3a | ||
|
|
a26fc9169c | ||
|
|
967a87a65f | ||
|
|
e68f7ba96a | ||
|
|
3405d19be8 | ||
|
|
f27de1c29e | ||
|
|
9a185b2388 | ||
|
|
caf6fc63c1 | ||
|
|
cf352f7c8a | ||
|
|
ac5bed210f | ||
|
|
a72d43526f | ||
|
|
9eb2457aa4 | ||
|
|
b83402b886 | ||
|
|
078564bd38 | ||
|
|
a2c08d312e | ||
|
|
f7a3d38d6b | ||
|
|
a3b8585697 | ||
|
|
2e526e9b86 | ||
|
|
541fe70b78 | ||
|
|
c786b697bf | ||
|
|
aa6c00b083 | ||
|
|
17855e3d8e | ||
|
|
842809bef5 | ||
|
|
4e567ee007 | ||
|
|
56700e120b | ||
|
|
5261444265 | ||
|
|
b3aa291182 | ||
|
|
051b474bf0 | ||
|
|
fac6632459 | ||
|
|
1c7cf98e58 | ||
|
|
44a54e070d | ||
|
|
10b9a6c013 | ||
|
|
a3b67e4607 | ||
|
|
ac1ce18143 | ||
|
|
01a305d326 | ||
|
|
233259be40 | ||
|
|
107def9ccc | ||
|
|
d46ce783c2 | ||
|
|
d743472b16 | ||
|
|
d785b263f4 | ||
|
|
6d37a4e333 | ||
|
|
eb49e51ffb | ||
|
|
734d4bfc0c | ||
|
|
0e75bbda58 | ||
|
|
8b5eb21ac3 | ||
|
|
c958e4c34e | ||
|
|
ab48d830d1 | ||
|
|
915579c0a4 | ||
|
|
0a6e6f02a4 | ||
|
|
a135f45844 | ||
|
|
b39b91774d | ||
|
|
2275248813 | ||
|
|
75f4a91f11 | ||
|
|
d0b5c6c854 | ||
|
|
bd8940ae0a | ||
|
|
1be64f0d11 | ||
|
|
1c8f4b3a35 | ||
|
|
3e5176a79c | ||
|
|
53c0f2a9df | ||
|
|
28389aa38c | ||
|
|
fb5c825cf6 | ||
|
|
a0e0da1da9 | ||
|
|
a826859202 | ||
|
|
71b4a1de94 | ||
|
|
93a2e4b1ca | ||
|
|
436266b1a4 | ||
|
|
19427e33a0 | ||
|
|
48d683be77 | ||
|
|
e1ec264345 | ||
|
|
9aad2efdb0 | ||
|
|
f6595a02c4 | ||
|
|
92ca9bf7eb | ||
|
|
05242a83f0 | ||
|
|
add44c0f18 | ||
|
|
a1ebf3f90e | ||
|
|
626d50d3e9 | ||
|
|
eaa7ad85f8 | ||
|
|
be9be0b2d1 | ||
|
|
f196999c67 | ||
|
|
e674ec4f33 | ||
|
|
f23805550f | ||
|
|
55a356bbec | ||
|
|
954aaeb06b | ||
|
|
27e272c58f | ||
|
|
3ed9912bcb | ||
|
|
e0f6563a39 | ||
|
|
caa64724d0 | ||
|
|
a2011b8bc7 | ||
|
|
40fd9e935a | ||
|
|
b88c143914 | ||
|
|
0a05007c4e | ||
|
|
c47ba3f804 | ||
|
|
79a3f361a7 | ||
|
|
65399a37e5 | ||
|
|
25a3fcf731 | ||
|
|
17e2c2df8e | ||
|
|
240345d527 | ||
|
|
2d284d9277 | ||
|
|
bfa5c5944f | ||
|
|
e1051cd40f | ||
|
|
13029cc7fe | ||
|
|
58d9e4e194 | ||
|
|
4de7036e2f | ||
|
|
617b1b2863 | ||
|
|
e59a360b60 | ||
|
|
91cd53a878 | ||
|
|
c6f2c7b617 | ||
|
|
568bfb2513 | ||
|
|
66ff8ad7ec | ||
|
|
400d9471b6 | ||
|
|
288380ca8d | ||
|
|
429d5b9101 | ||
|
|
1be452d7c0 | ||
|
|
671c944272 | ||
|
|
a48a9031cc | ||
|
|
aff6943d07 | ||
|
|
f56ff96b88 | ||
|
|
b5361c2a28 | ||
|
|
e71b6cacce | ||
|
|
8a1297d3c5 | ||
|
|
bdea7d5853 | ||
|
|
389dee0f3c | ||
|
|
b49e174483 | ||
|
|
53b612ab07 | ||
|
|
446708e456 | ||
|
|
1233ff0ead | ||
|
|
af3f1e2a85 | ||
|
|
4dbc5653ff | ||
|
|
e8d9f3327c | ||
|
|
d372043a17 | ||
|
|
eeac65da8c | ||
|
|
c1780ae30a | ||
|
|
fb39d69fa7 | ||
|
|
6392d63ff8 | ||
|
|
b009a260a4 | ||
|
|
44379cd9fd | ||
|
|
b85ca680f0 | ||
|
|
8fb7a8988f | ||
|
|
6d097c2085 | ||
|
|
f9d68d688d | ||
|
|
bc08b85191 | ||
|
|
f3d26e038d | ||
|
|
b4e1fbf290 | ||
|
|
58d5fba945 | ||
|
|
c76036a6f2 | ||
|
|
6a8848d1e2 | ||
|
|
02d1050130 | ||
|
|
ccb44c964c | ||
|
|
b2165b6a29 | ||
|
|
d81d10ec27 | ||
|
|
9529c04680 | ||
|
|
6a89c9af38 | ||
|
|
06a29fc912 | ||
|
|
2cba7253fc | ||
|
|
47d60fe51b | ||
|
|
28f16c9a17 | ||
|
|
6560768e08 | ||
|
|
084c0c4ec8 |
19
.circleci/config.yml
Normal file
19
.circleci/config.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
working_directory: ~/build
|
||||
docker:
|
||||
- image: circleci/golang:latest
|
||||
steps:
|
||||
- checkout
|
||||
- setup_remote_docker:
|
||||
version: 19.03.13
|
||||
- run: make image
|
||||
- deploy:
|
||||
command: |
|
||||
if [[ "$CIRCLE_BRANCH" == "master" ]]; then
|
||||
docker tag ctop quay.io/vektorlab/ctop:latest
|
||||
docker tag ctop quay.io/vektorlab/ctop:$(cat VERSION)
|
||||
docker login -u $DOCKER_USER -p $DOCKER_PASS quay.io
|
||||
docker push quay.io/vektorlab/ctop
|
||||
fi
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
ctop
|
||||
.idea
|
||||
.idea
|
||||
/vendor/
|
||||
16
Dockerfile
16
Dockerfile
@@ -1,3 +1,17 @@
|
||||
FROM quay.io/vektorcloud/go:1.13
|
||||
|
||||
RUN apk add --no-cache make
|
||||
|
||||
WORKDIR /app
|
||||
COPY go.mod .
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
RUN make build && \
|
||||
mkdir -p /go/bin && \
|
||||
mv -v ctop /go/bin/
|
||||
|
||||
FROM scratch
|
||||
COPY ./ctop /ctop
|
||||
ENV TERM=linux
|
||||
COPY --from=0 /go/bin/ctop /ctop
|
||||
ENTRYPOINT ["/ctop"]
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
FROM quay.io/vektorcloud/go:1.8
|
||||
|
||||
RUN apk add --no-cache make
|
||||
|
||||
COPY glide.* /go/src/github.com/bcicen/ctop/
|
||||
WORKDIR /go/src/github.com/bcicen/ctop/
|
||||
RUN glide install
|
||||
|
||||
COPY . /go/src/github.com/bcicen/ctop
|
||||
RUN make build && \
|
||||
mkdir -p /go/bin && \
|
||||
mv -v ctop /go/bin/
|
||||
28
Makefile
28
Makefile
@@ -4,30 +4,34 @@ BUILD=$(shell git rev-parse --short HEAD)
|
||||
LD_FLAGS="-w -X main.version=$(VERSION) -X main.build=$(BUILD)"
|
||||
|
||||
clean:
|
||||
rm -rf build/ release/
|
||||
rm -rf _build/ release/
|
||||
|
||||
build:
|
||||
glide install
|
||||
go mod download
|
||||
CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o ctop
|
||||
|
||||
build-dev:
|
||||
go build -ldflags "-w -X main.version=$(VERSION)-dev -X main.build=$(BUILD)"
|
||||
|
||||
build-all:
|
||||
mkdir -p build
|
||||
GOOS=darwin GOARCH=amd64 go build -tags release -ldflags $(LD_FLAGS) -o build/ctop-$(VERSION)-darwin-amd64
|
||||
GOOS=linux GOARCH=amd64 go build -tags release -ldflags $(LD_FLAGS) -o build/ctop-$(VERSION)-linux-amd64
|
||||
GOOS=linux GOARCH=arm go build -tags release -ldflags $(LD_FLAGS) -o build/ctop-$(VERSION)-linux-arm
|
||||
mkdir -p _build
|
||||
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-darwin-amd64
|
||||
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-amd64
|
||||
GOOS=linux GOARCH=arm CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-arm
|
||||
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-arm64
|
||||
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-windows-amd64
|
||||
cd _build; sha256sum * > sha256sums.txt
|
||||
|
||||
run-dev:
|
||||
rm -f ctop.sock ctop
|
||||
go build -ldflags $(LD_FLAGS) -o ctop
|
||||
CTOP_DEBUG=1 ./ctop
|
||||
|
||||
image:
|
||||
docker build -t ctop_build -f Dockerfile_build .
|
||||
docker run -ti --rm -v $(shell pwd):/target ctop_build cp -v /go/bin/ctop /target/
|
||||
docker build -t ctop -f Dockerfile .
|
||||
|
||||
release:
|
||||
mkdir release
|
||||
go get github.com/progrium/gh-release/...
|
||||
cp build/* release
|
||||
cp _build/* release
|
||||
cd release; sha256sum --quiet --check sha256sums.txt
|
||||
gh-release create bcicen/$(NAME) $(VERSION) \
|
||||
$(shell git rev-parse --abbrev-ref HEAD) $(VERSION)
|
||||
|
||||
|
||||
75
README.md
75
README.md
@@ -2,14 +2,16 @@
|
||||
|
||||
#
|
||||
|
||||
![release][release] ![homebrew][homebrew]
|
||||
|
||||
Top-like interface for container metrics
|
||||
|
||||
`ctop` provides a concise and condensed overview of real-time metrics for multiple containers:
|
||||
<p align="center"><img src="_docs/img/grid.gif" alt="ctop"/></p>
|
||||
|
||||
as well as an [expanded view][expanded_view] for inspecting a specific container.
|
||||
as well as a [single container view][single_view] for inspecting a specific container.
|
||||
|
||||
`ctop` currently comes with built-in support for Docker; connectors for other container and cluster systems are planned for future releases.
|
||||
`ctop` comes with built-in support for Docker and runC; connectors for other container and cluster systems are planned for future releases.
|
||||
|
||||
## Install
|
||||
|
||||
@@ -18,7 +20,7 @@ Fetch the [latest release](https://github.com/bcicen/ctop/releases) for your pla
|
||||
#### Linux
|
||||
|
||||
```bash
|
||||
sudo wget https://github.com/bcicen/ctop/releases/download/v0.5/ctop-0.5-linux-amd64 -O /usr/local/bin/ctop
|
||||
sudo wget https://github.com/bcicen/ctop/releases/download/v0.7.4/ctop-0.7.4-linux-amd64 -O /usr/local/bin/ctop
|
||||
sudo chmod +x /usr/local/bin/ctop
|
||||
```
|
||||
|
||||
@@ -29,13 +31,17 @@ brew install ctop
|
||||
```
|
||||
or
|
||||
```bash
|
||||
sudo curl -Lo /usr/local/bin/ctop https://github.com/bcicen/ctop/releases/download/v0.5/ctop-0.5-darwin-amd64
|
||||
sudo curl -Lo /usr/local/bin/ctop https://github.com/bcicen/ctop/releases/download/v0.7.4/ctop-0.7.4-darwin-amd64
|
||||
sudo chmod +x /usr/local/bin/ctop
|
||||
```
|
||||
|
||||
or run via Docker:
|
||||
#### Docker
|
||||
|
||||
```bash
|
||||
docker run -ti --name ctop --rm -v /var/run/docker.sock:/var/run/docker.sock quay.io/vektorlab/ctop:latest
|
||||
docker run --rm -ti \
|
||||
--name=ctop \
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock:ro \
|
||||
quay.io/vektorlab/ctop:latest
|
||||
```
|
||||
|
||||
`ctop` is also available for Arch in the [AUR](https://aur.archlinux.org/packages/ctop-bin/)
|
||||
@@ -46,35 +52,48 @@ Build steps can be found [here][build].
|
||||
|
||||
## Usage
|
||||
|
||||
`ctop` requires no arguments and will configure itself using the `DOCKER_HOST` environment variable
|
||||
```bash
|
||||
export DOCKER_HOST=tcp://127.0.0.1:4243
|
||||
ctop
|
||||
```
|
||||
`ctop` requires no arguments and uses Docker host variables by default. See [connectors][connectors] for further configuration options.
|
||||
|
||||
### Config file
|
||||
|
||||
While running, use `S` to save the current filters, sort field, and other options to a default config path (`~/.config/ctop/config` on XDG systems, else `~/.ctop`).
|
||||
|
||||
Config file values will be loaded and applied the next time `ctop` is started.
|
||||
|
||||
### Options
|
||||
|
||||
Option | Description
|
||||
--- | ---
|
||||
-a | show active containers only
|
||||
-f <string> | set an initial filter string
|
||||
-h | display help dialog
|
||||
-i | invert default colors
|
||||
-r | reverse container sort order
|
||||
-s | select initial container sort field
|
||||
-v | output version information and exit
|
||||
`-a` | show active containers only
|
||||
`-f <string>` | set an initial filter string
|
||||
`-h` | display help dialog
|
||||
`-i` | invert default colors
|
||||
`-r` | reverse container sort order
|
||||
`-s` | select initial container sort field
|
||||
`-scale-cpu` | show cpu as % of system total
|
||||
`-v` | output version information and exit
|
||||
`-shell` | exec shell to use (default: sh)
|
||||
|
||||
### Keybindings
|
||||
|
||||
Key | Action
|
||||
--- | ---
|
||||
a | Toggle display of all (running and non-running) containers
|
||||
f | Filter displayed containers (`esc` to clear when open)
|
||||
H | Toggle ctop header
|
||||
h | Open help dialog
|
||||
s | Select container sort field
|
||||
r | Reverse container sort order
|
||||
q | Quit ctop
|
||||
| Key | Action |
|
||||
| :----------------------: | ---------------------------------------------------------- |
|
||||
| <kbd><ENTER></kbd> | Open container menu |
|
||||
| <kbd>a</kbd> | Toggle display of all (running and non-running) containers |
|
||||
| <kbd>f</kbd> | Filter displayed containers (`esc` to clear when open) |
|
||||
| <kbd>H</kbd> | Toggle ctop header |
|
||||
| <kbd>h</kbd> | Open help dialog |
|
||||
| <kbd>s</kbd> | Select container sort field |
|
||||
| <kbd>r</kbd> | Reverse container sort order |
|
||||
| <kbd>o</kbd> | Open single view |
|
||||
| <kbd>l</kbd> | View container logs (`t` to toggle timestamp when open) |
|
||||
| <kbd>e</kbd> | Exec Shell |
|
||||
| <kbd>c</kbd> | Configure columns |
|
||||
| <kbd>S</kbd> | Save current configuration to file |
|
||||
| <kbd>q</kbd> | Quit ctop |
|
||||
|
||||
[build]: _docs/build.md
|
||||
[expanded_view]: _docs/expanded.md
|
||||
[connectors]: _docs/connectors.md
|
||||
[single_view]: _docs/single.md
|
||||
[release]: https://img.shields.io/github/release/bcicen/ctop.svg "ctop"
|
||||
[homebrew]: https://img.shields.io/homebrew/v/ctop.svg "ctop"
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
# Build
|
||||
|
||||
To build `ctop` from source, ensure you have a recent version of [glide](https://github.com/Masterminds/glide) installed and run:
|
||||
To build `ctop` from source, simply clone the repo and run:
|
||||
|
||||
```bash
|
||||
go get github.com/bcicen/ctop && \
|
||||
cd $GOPATH/src/github.com/bcicen/ctop && \
|
||||
make build
|
||||
```
|
||||
|
||||
@@ -16,5 +14,8 @@ make image
|
||||
Now you can run your local image:
|
||||
|
||||
```bash
|
||||
docker run -ti --name ctop --rm -v /var/run/docker.sock:/var/run/docker.sock ctop
|
||||
docker run --rm -ti \
|
||||
--name ctop \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
ctop:latest
|
||||
```
|
||||
|
||||
26
_docs/connectors.md
Normal file
26
_docs/connectors.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Connectors
|
||||
|
||||
`ctop` comes with the below native connectors, enabled via the `--connector` option.
|
||||
|
||||
Default connector behavior can be changed by setting the relevant environment variables.
|
||||
|
||||
## Docker
|
||||
|
||||
Default connector, configurable via standard [Docker commandline varaibles](https://docs.docker.com/engine/reference/commandline/cli/#environment-variables)
|
||||
|
||||
#### Options
|
||||
|
||||
Var | Description
|
||||
--- | ---
|
||||
DOCKER_HOST | Daemon socket to connect to (default: `unix://var/run/docker.sock`)
|
||||
|
||||
## RunC
|
||||
|
||||
Using this connector requires full privileges to the local runC root dir of container state (default: `/run/runc`)
|
||||
|
||||
#### Options
|
||||
|
||||
Var | Description
|
||||
--- | ---
|
||||
RUNC_ROOT | path to runc root for container state (default: `/run/runc`)
|
||||
RUNC_SYSTEMD_CGROUP | if set, enable systemd cgroups
|
||||
@@ -1,14 +1,23 @@
|
||||
# Debug Mode
|
||||
|
||||
`ctop` comes with a built-in logging facility and local socket server to simplify debugging at run time. Debug mode can be enabled via the `CTOP_DEBUG` environment variable:
|
||||
`ctop` comes with a built-in logging facility and local socket server to simplify debugging at run time.
|
||||
|
||||
## Quick Start
|
||||
|
||||
If running `ctop` via Docker, debug logging can be most easily enabled as below:
|
||||
```bash
|
||||
CTOP_DEBUG=1 ./ctop
|
||||
docker run -ti --rm \
|
||||
--name=ctop \
|
||||
-e CTOP_DEBUG=1 \
|
||||
-e CTOP_DEBUG_TCP=1 \
|
||||
-p 9000:9000 \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
quay.io/vektorlab/ctop:latest
|
||||
```
|
||||
|
||||
While `ctop` is running, you can connect to the logging socket via socat or similar tools:
|
||||
Log messages can be followed by connecting to the default listen address:
|
||||
```bash
|
||||
socat unix-connect:/tmp/ctop.sock stdio
|
||||
curl -s localhost:9000
|
||||
```
|
||||
|
||||
example output:
|
||||
@@ -22,3 +31,26 @@ example output:
|
||||
15:06:43.883 ▶ INFO 008 collector started for container: 7120f83ca...
|
||||
...
|
||||
```
|
||||
|
||||
## Unix Socket
|
||||
|
||||
Debug mode is enabled via the `CTOP_DEBUG` environment variable:
|
||||
|
||||
```bash
|
||||
CTOP_DEBUG=1 ./ctop
|
||||
```
|
||||
|
||||
While `ctop` is running, you can connect to the logging socket via socat or similar tools:
|
||||
```bash
|
||||
socat unix-connect:./ctop.sock stdio
|
||||
```
|
||||
|
||||
## TCP Logging Socket
|
||||
|
||||
In lieu of using a local unix socket, TCP logging can be enabled via the `CTOP_DEBUG_TCP` environment variable:
|
||||
|
||||
```bash
|
||||
CTOP_DEBUG=1 CTOP_DEBUG_TCP=1 ./ctop
|
||||
```
|
||||
|
||||
A TCP listener for streaming log messages will be started on the default listen address(`0.0.0.0:9000`)
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
# Expanded View
|
||||
|
||||
ctop provides an expanded, rolling view for following container metrics
|
||||
<p align="center"><img width="80%" src="img/expanded.gif" alt="ctop"/></p>
|
||||
|
Before Width: | Height: | Size: 549 KiB After Width: | Height: | Size: 549 KiB |
BIN
_docs/img/status.png
Normal file
BIN
_docs/img/status.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
4
_docs/single.md
Normal file
4
_docs/single.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Single Container View
|
||||
|
||||
ctop provides a rolling, single container view for following metrics
|
||||
<p align="center"><img width="80%" src="img/single.gif" alt="ctop"/></p>
|
||||
30
_docs/status.md
Normal file
30
_docs/status.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Status Indicator
|
||||
|
||||
The `ctop` grid view provides a compact status indicator to convey container state
|
||||
|
||||
<img width="200px" src="img/status.png" alt="ctop"/>
|
||||
|
||||
### Status
|
||||
|
||||
<span align="center">
|
||||
|
||||
Appearance | Description
|
||||
--- | ---
|
||||
red | container is stopped
|
||||
green | container is running
|
||||
▮▮ | container is paused
|
||||
|
||||
</span>
|
||||
|
||||
### Health
|
||||
If the container is configured with a health check, a `+` will appear next to the indicator
|
||||
|
||||
<span align="center">
|
||||
|
||||
Appearance | Description
|
||||
--- | ---
|
||||
red | health check in failed state
|
||||
yellow | health check in starting state
|
||||
green | health check in OK state
|
||||
|
||||
</span>
|
||||
23
circle.yml
23
circle.yml
@@ -1,23 +0,0 @@
|
||||
machine:
|
||||
services:
|
||||
- docker
|
||||
environment:
|
||||
IMAGE_NAME: quay.io/vektorlab/ctop
|
||||
|
||||
dependencies:
|
||||
override:
|
||||
- docker info
|
||||
- make image
|
||||
|
||||
test:
|
||||
override:
|
||||
- docker run -ti ctop -v
|
||||
|
||||
deployment:
|
||||
hub:
|
||||
branch: master
|
||||
commands:
|
||||
- docker tag ctop ${IMAGE_NAME}:latest
|
||||
- docker tag ctop ${IMAGE_NAME}:$(cat VERSION)
|
||||
- docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS quay.io
|
||||
- docker push ${IMAGE_NAME}
|
||||
@@ -45,11 +45,14 @@ var ColorMap = map[string]ui.Attribute{
|
||||
"par.text.hi": ui.ColorBlack,
|
||||
"sparkline.line.fg": ui.ColorGreen,
|
||||
"sparkline.title.fg": ui.ColorWhite,
|
||||
"status.ok": ui.ColorGreen,
|
||||
"status.warn": ui.ColorYellow,
|
||||
"status.danger": ui.ColorRed,
|
||||
}
|
||||
|
||||
func InvertColorMap() {
|
||||
re := regexp.MustCompile(".*.fg")
|
||||
for k, _ := range ColorMap {
|
||||
for k := range ColorMap {
|
||||
if re.FindAllString(k, 1) != nil {
|
||||
ColorMap[k] = ui.ColorBlack
|
||||
}
|
||||
|
||||
145
config/columns.go
Normal file
145
config/columns.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// defaults
|
||||
var defaultColumns = []Column{
|
||||
Column{
|
||||
Name: "status",
|
||||
Label: "Status Indicator",
|
||||
Enabled: true,
|
||||
},
|
||||
Column{
|
||||
Name: "name",
|
||||
Label: "Container Name",
|
||||
Enabled: true,
|
||||
},
|
||||
Column{
|
||||
Name: "id",
|
||||
Label: "Container ID",
|
||||
Enabled: true,
|
||||
},
|
||||
Column{
|
||||
Name: "cpu",
|
||||
Label: "CPU Usage",
|
||||
Enabled: true,
|
||||
},
|
||||
Column{
|
||||
Name: "mem",
|
||||
Label: "Memory Usage",
|
||||
Enabled: true,
|
||||
},
|
||||
Column{
|
||||
Name: "net",
|
||||
Label: "Network RX/TX",
|
||||
Enabled: true,
|
||||
},
|
||||
Column{
|
||||
Name: "io",
|
||||
Label: "Disk IO Read/Write",
|
||||
Enabled: true,
|
||||
},
|
||||
Column{
|
||||
Name: "pids",
|
||||
Label: "Container PID Count",
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
type Column struct {
|
||||
Name string
|
||||
Label string
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
// ColumnsString returns an ordered and comma-delimited string of currently enabled Columns
|
||||
func ColumnsString() string { return strings.Join(EnabledColumns(), ",") }
|
||||
|
||||
// EnabledColumns returns an ordered array of enabled column names
|
||||
func EnabledColumns() (a []string) {
|
||||
lock.RLock()
|
||||
defer lock.RUnlock()
|
||||
for _, col := range GlobalColumns {
|
||||
if col.Enabled {
|
||||
a = append(a, col.Name)
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// ColumnToggle toggles the enabled status of a given column name
|
||||
func ColumnToggle(name string) {
|
||||
col := GlobalColumns[colIndex(name)]
|
||||
col.Enabled = !col.Enabled
|
||||
log.Noticef("config change [column-%s]: %t -> %t", col.Name, !col.Enabled, col.Enabled)
|
||||
}
|
||||
|
||||
// ColumnLeft moves the column with given name up one position, if possible
|
||||
func ColumnLeft(name string) {
|
||||
idx := colIndex(name)
|
||||
if idx > 0 {
|
||||
swapCols(idx, idx-1)
|
||||
}
|
||||
}
|
||||
|
||||
// ColumnRight moves the column with given name up one position, if possible
|
||||
func ColumnRight(name string) {
|
||||
idx := colIndex(name)
|
||||
if idx < len(GlobalColumns)-1 {
|
||||
swapCols(idx, idx+1)
|
||||
}
|
||||
}
|
||||
|
||||
// Set Column order and enabled status from one or more provided Column names
|
||||
func SetColumns(names []string) {
|
||||
var (
|
||||
n int
|
||||
curColStr = ColumnsString()
|
||||
newColumns = make([]*Column, len(GlobalColumns))
|
||||
)
|
||||
|
||||
lock.Lock()
|
||||
|
||||
// add enabled columns by name
|
||||
for _, name := range names {
|
||||
newColumns[n] = popColumn(name)
|
||||
newColumns[n].Enabled = true
|
||||
n++
|
||||
}
|
||||
|
||||
// extend with omitted columns as disabled
|
||||
for _, col := range GlobalColumns {
|
||||
newColumns[n] = col
|
||||
newColumns[n].Enabled = false
|
||||
n++
|
||||
}
|
||||
|
||||
GlobalColumns = newColumns
|
||||
lock.Unlock()
|
||||
|
||||
log.Noticef("config change [columns]: %s -> %s", curColStr, ColumnsString())
|
||||
}
|
||||
|
||||
func swapCols(i, j int) { GlobalColumns[i], GlobalColumns[j] = GlobalColumns[j], GlobalColumns[i] }
|
||||
|
||||
func popColumn(name string) *Column {
|
||||
idx := colIndex(name)
|
||||
if idx < 0 {
|
||||
panic("no such column name: " + name)
|
||||
}
|
||||
col := GlobalColumns[idx]
|
||||
GlobalColumns = append(GlobalColumns[:idx], GlobalColumns[idx+1:]...)
|
||||
return col
|
||||
}
|
||||
|
||||
// return index of column with given name, if any
|
||||
func colIndex(name string) int {
|
||||
for n, c := range GlobalColumns {
|
||||
if c.Name == name {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
148
config/file.go
Normal file
148
config/file.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
var (
|
||||
xdgRe = regexp.MustCompile("^XDG_*")
|
||||
)
|
||||
|
||||
type File struct {
|
||||
Options map[string]string `toml:"options"`
|
||||
Toggles map[string]bool `toml:"toggles"`
|
||||
}
|
||||
|
||||
func exportConfig() File {
|
||||
// update columns param from working config
|
||||
Update("columns", ColumnsString())
|
||||
|
||||
lock.RLock()
|
||||
defer lock.RUnlock()
|
||||
|
||||
c := File{
|
||||
Options: make(map[string]string),
|
||||
Toggles: make(map[string]bool),
|
||||
}
|
||||
|
||||
for _, p := range GlobalParams {
|
||||
c.Options[p.Key] = p.Val
|
||||
}
|
||||
for _, sw := range GlobalSwitches {
|
||||
c.Toggles[sw.Key] = sw.Val
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
//
|
||||
func Read() error {
|
||||
var config File
|
||||
|
||||
path, err := getConfigPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := toml.DecodeFile(path, &config); err != nil {
|
||||
return err
|
||||
}
|
||||
for k, v := range config.Options {
|
||||
Update(k, v)
|
||||
}
|
||||
for k, v := range config.Toggles {
|
||||
UpdateSwitch(k, v)
|
||||
}
|
||||
|
||||
// set working column config, if provided
|
||||
colStr := GetVal("columns")
|
||||
if len(colStr) > 0 {
|
||||
var colNames []string
|
||||
for _, s := range strings.Split(colStr, ",") {
|
||||
s = strings.TrimSpace(s)
|
||||
if s != "" {
|
||||
colNames = append(colNames, strings.TrimSpace(s))
|
||||
}
|
||||
}
|
||||
SetColumns(colNames)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Write() (path string, err error) {
|
||||
path, err = getConfigPath()
|
||||
if err != nil {
|
||||
return path, err
|
||||
}
|
||||
|
||||
cfgdir := basedir(path)
|
||||
// create config dir if not exist
|
||||
if _, err := os.Stat(cfgdir); err != nil {
|
||||
err = os.MkdirAll(cfgdir, 0755)
|
||||
if err != nil {
|
||||
return path, fmt.Errorf("failed to create config dir [%s]: %s", cfgdir, err)
|
||||
}
|
||||
}
|
||||
|
||||
// remove prior to writing new file
|
||||
if err := os.Remove(path); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return path, err
|
||||
}
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return path, fmt.Errorf("failed to open config for writing: %s", err)
|
||||
}
|
||||
|
||||
writer := toml.NewEncoder(file)
|
||||
err = writer.Encode(exportConfig())
|
||||
if err != nil {
|
||||
return path, fmt.Errorf("failed to write config: %s", err)
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// determine config path from environment
|
||||
func getConfigPath() (path string, err error) {
|
||||
homeDir, ok := os.LookupEnv("HOME")
|
||||
if !ok {
|
||||
return path, fmt.Errorf("$HOME not set")
|
||||
}
|
||||
|
||||
// use xdg config home if possible
|
||||
if xdgSupport() {
|
||||
xdgHome, ok := os.LookupEnv("XDG_CONFIG_HOME")
|
||||
if !ok {
|
||||
xdgHome = fmt.Sprintf("%s/.config", homeDir)
|
||||
}
|
||||
path = fmt.Sprintf("%s/ctop/config", xdgHome)
|
||||
} else {
|
||||
path = fmt.Sprintf("%s/.ctop", homeDir)
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// test for environemnt supporting XDG spec
|
||||
func xdgSupport() bool {
|
||||
for _, e := range os.Environ() {
|
||||
if xdgRe.FindAllString(e, 1) != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func basedir(path string) string {
|
||||
parts := strings.Split(path, "/")
|
||||
return strings.Join((parts[0 : len(parts)-1]), "/")
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package config
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/bcicen/ctop/logging"
|
||||
)
|
||||
@@ -10,17 +11,24 @@ import (
|
||||
var (
|
||||
GlobalParams []*Param
|
||||
GlobalSwitches []*Switch
|
||||
GlobalColumns []*Column
|
||||
lock sync.RWMutex
|
||||
log = logging.Init()
|
||||
)
|
||||
|
||||
func Init() {
|
||||
for _, p := range params {
|
||||
for _, p := range defaultParams {
|
||||
GlobalParams = append(GlobalParams, p)
|
||||
log.Infof("loaded config param: %s: %s", quote(p.Key), quote(p.Val))
|
||||
log.Infof("loaded default config param [%s]: %s", quote(p.Key), quote(p.Val))
|
||||
}
|
||||
for _, s := range switches {
|
||||
for _, s := range defaultSwitches {
|
||||
GlobalSwitches = append(GlobalSwitches, s)
|
||||
log.Infof("loaded config switch: %s: %t", quote(s.Key), s.Val)
|
||||
log.Infof("loaded default config switch [%s]: %t", quote(s.Key), s.Val)
|
||||
}
|
||||
for _, c := range defaultColumns {
|
||||
x := c
|
||||
GlobalColumns = append(GlobalColumns, &x)
|
||||
log.Infof("loaded default widget config [%s]: %t", quote(x.Name), x.Enabled)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package config
|
||||
|
||||
// defaults
|
||||
var params = []*Param{
|
||||
var defaultParams = []*Param{
|
||||
&Param{
|
||||
Key: "filterStr",
|
||||
Val: "",
|
||||
@@ -12,6 +12,16 @@ var params = []*Param{
|
||||
Val: "state",
|
||||
Label: "Container Sort Field",
|
||||
},
|
||||
&Param{
|
||||
Key: "shell",
|
||||
Val: "sh",
|
||||
Label: "Shell",
|
||||
},
|
||||
&Param{
|
||||
Key: "columns",
|
||||
Val: "status,name,id,cpu,mem,net,io,pids",
|
||||
Label: "Enabled Columns",
|
||||
},
|
||||
}
|
||||
|
||||
type Param struct {
|
||||
@@ -22,6 +32,9 @@ type Param struct {
|
||||
|
||||
// Get Param by key
|
||||
func Get(k string) *Param {
|
||||
lock.RLock()
|
||||
defer lock.RUnlock()
|
||||
|
||||
for _, p := range GlobalParams {
|
||||
if p.Key == k {
|
||||
return p
|
||||
@@ -30,7 +43,7 @@ func Get(k string) *Param {
|
||||
return &Param{} // default
|
||||
}
|
||||
|
||||
// Get Param value by key
|
||||
// GetVal gets Param value by key
|
||||
func GetVal(k string) string {
|
||||
return Get(k).Val
|
||||
}
|
||||
@@ -38,7 +51,10 @@ func GetVal(k string) string {
|
||||
// Set param value
|
||||
func Update(k, v string) {
|
||||
p := Get(k)
|
||||
log.Noticef("config change: %s: %s -> %s", k, quote(p.Val), quote(v))
|
||||
log.Noticef("config change [%s]: %s -> %s", k, quote(p.Val), quote(v))
|
||||
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
p.Val = v
|
||||
// log.Errorf("ignoring update for non-existant parameter: %s", k)
|
||||
}
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
package config
|
||||
|
||||
// defaults
|
||||
var switches = []*Switch{
|
||||
var defaultSwitches = []*Switch{
|
||||
&Switch{
|
||||
Key: "sortReversed",
|
||||
Val: false,
|
||||
Label: "Reverse Sort Order",
|
||||
Label: "Reverse sort order",
|
||||
},
|
||||
&Switch{
|
||||
Key: "allContainers",
|
||||
Val: true,
|
||||
Label: "Show All Containers",
|
||||
Label: "Show all containers",
|
||||
},
|
||||
&Switch{
|
||||
Key: "fullRowCursor",
|
||||
Val: true,
|
||||
Label: "Highlight entire cursor row (vs. name only)",
|
||||
},
|
||||
&Switch{
|
||||
Key: "enableHeader",
|
||||
Val: true,
|
||||
Label: "Enable Status Header",
|
||||
Label: "Enable status header",
|
||||
},
|
||||
&Switch{
|
||||
Key: "scaleCpu",
|
||||
Val: false,
|
||||
Label: "Show CPU as %% of system total",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -25,8 +35,11 @@ type Switch struct {
|
||||
Label string
|
||||
}
|
||||
|
||||
// Return Switch by key
|
||||
// GetSwitch returns Switch by key
|
||||
func GetSwitch(k string) *Switch {
|
||||
lock.RLock()
|
||||
defer lock.RUnlock()
|
||||
|
||||
for _, sw := range GlobalSwitches {
|
||||
if sw.Key == k {
|
||||
return sw
|
||||
@@ -35,16 +48,31 @@ func GetSwitch(k string) *Switch {
|
||||
return &Switch{} // default
|
||||
}
|
||||
|
||||
// Return Switch value by key
|
||||
// GetSwitchVal returns Switch value by key
|
||||
func GetSwitchVal(k string) bool {
|
||||
return GetSwitch(k).Val
|
||||
}
|
||||
|
||||
func UpdateSwitch(k string, val bool) {
|
||||
sw := GetSwitch(k)
|
||||
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
if sw.Val != val {
|
||||
log.Noticef("config change [%s]: %t -> %t", k, sw.Val, val)
|
||||
sw.Val = val
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle a boolean switch
|
||||
func Toggle(k string) {
|
||||
sw := GetSwitch(k)
|
||||
newVal := sw.Val != true
|
||||
log.Noticef("config change: %s: %t -> %t", k, sw.Val, newVal)
|
||||
sw.Val = newVal
|
||||
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
sw.Val = !sw.Val
|
||||
log.Noticef("config change [%s]: %t -> %t", k, !sw.Val, sw.Val)
|
||||
//log.Errorf("ignoring toggle for non-existant switch: %s", k)
|
||||
}
|
||||
|
||||
@@ -1,32 +1,36 @@
|
||||
package metrics
|
||||
package collector
|
||||
|
||||
import (
|
||||
"github.com/bcicen/ctop/config"
|
||||
"github.com/bcicen/ctop/models"
|
||||
api "github.com/fsouza/go-dockerclient"
|
||||
)
|
||||
|
||||
// Docker collector
|
||||
type Docker struct {
|
||||
Metrics
|
||||
models.Metrics
|
||||
id string
|
||||
client *api.Client
|
||||
running bool
|
||||
stream chan Metrics
|
||||
stream chan models.Metrics
|
||||
done chan bool
|
||||
lastCpu float64
|
||||
lastSysCpu float64
|
||||
scaleCpu bool
|
||||
}
|
||||
|
||||
func NewDocker(client *api.Client, id string) *Docker {
|
||||
return &Docker{
|
||||
Metrics: Metrics{},
|
||||
id: id,
|
||||
client: client,
|
||||
Metrics: models.Metrics{},
|
||||
id: id,
|
||||
client: client,
|
||||
scaleCpu: config.GetSwitchVal("scaleCpu"),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Docker) Start() {
|
||||
c.done = make(chan bool)
|
||||
c.stream = make(chan Metrics)
|
||||
c.stream = make(chan models.Metrics)
|
||||
stats := make(chan *api.Stats)
|
||||
|
||||
go func() {
|
||||
@@ -60,10 +64,14 @@ func (c *Docker) Running() bool {
|
||||
return c.running
|
||||
}
|
||||
|
||||
func (c *Docker) Stream() chan Metrics {
|
||||
func (c *Docker) Stream() chan models.Metrics {
|
||||
return c.stream
|
||||
}
|
||||
|
||||
func (c *Docker) Logs() LogCollector {
|
||||
return NewDockerLogs(c.id, c.client)
|
||||
}
|
||||
|
||||
// Stop collector
|
||||
func (c *Docker) Stop() {
|
||||
c.done <- true
|
||||
@@ -77,16 +85,20 @@ func (c *Docker) ReadCPU(stats *api.Stats) {
|
||||
cpudiff := total - c.lastCpu
|
||||
syscpudiff := system - c.lastSysCpu
|
||||
|
||||
c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
|
||||
if c.scaleCpu {
|
||||
c.CPUUtil = round((cpudiff / syscpudiff * 100))
|
||||
} else {
|
||||
c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
|
||||
}
|
||||
c.lastCpu = total
|
||||
c.lastSysCpu = system
|
||||
c.Pids = int(stats.PidsStats.Current)
|
||||
}
|
||||
|
||||
func (c *Docker) ReadMem(stats *api.Stats) {
|
||||
c.MemUsage = int64(stats.MemoryStats.Usage)
|
||||
c.MemUsage = int64(stats.MemoryStats.Usage - stats.MemoryStats.Stats.Cache)
|
||||
c.MemLimit = int64(stats.MemoryStats.Limit)
|
||||
c.MemPercent = round((float64(c.MemUsage) / float64(c.MemLimit)) * 100)
|
||||
c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
|
||||
}
|
||||
|
||||
func (c *Docker) ReadNet(stats *api.Stats) {
|
||||
@@ -102,10 +114,10 @@ func (c *Docker) ReadIO(stats *api.Stats) {
|
||||
var read, write int64
|
||||
for _, blk := range stats.BlkioStats.IOServiceBytesRecursive {
|
||||
if blk.Op == "Read" {
|
||||
read = int64(blk.Value)
|
||||
read += int64(blk.Value)
|
||||
}
|
||||
if blk.Op == "Write" {
|
||||
write = int64(blk.Value)
|
||||
write += int64(blk.Value)
|
||||
}
|
||||
}
|
||||
c.IOBytesRead, c.IOBytesWrite = read, write
|
||||
99
connector/collector/docker_logs.go
Normal file
99
connector/collector/docker_logs.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bcicen/ctop/models"
|
||||
api "github.com/fsouza/go-dockerclient"
|
||||
)
|
||||
|
||||
type DockerLogs struct {
|
||||
id string
|
||||
client *api.Client
|
||||
done chan bool
|
||||
}
|
||||
|
||||
func NewDockerLogs(id string, client *api.Client) *DockerLogs {
|
||||
return &DockerLogs{
|
||||
id: id,
|
||||
client: client,
|
||||
done: make(chan bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (l *DockerLogs) Stream() chan models.Log {
|
||||
r, w := io.Pipe()
|
||||
logCh := make(chan models.Log)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
opts := api.LogsOptions{
|
||||
Context: ctx,
|
||||
Container: l.id,
|
||||
OutputStream: w,
|
||||
//ErrorStream: w,
|
||||
Stdout: true,
|
||||
Stderr: true,
|
||||
Tail: "20",
|
||||
Follow: true,
|
||||
Timestamps: true,
|
||||
RawTerminal: true,
|
||||
}
|
||||
|
||||
// read io pipe into channel
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
parts := strings.Split(scanner.Text(), " ")
|
||||
ts := l.parseTime(parts[0])
|
||||
logCh <- models.Log{Timestamp: ts, Message: strings.Join(parts[1:], " ")}
|
||||
}
|
||||
}()
|
||||
|
||||
// connect to container log stream
|
||||
go func() {
|
||||
err := l.client.Logs(opts)
|
||||
if err != nil {
|
||||
log.Errorf("error reading container logs: %s", err)
|
||||
}
|
||||
log.Infof("log reader stopped for container: %s", l.id)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
<-l.done
|
||||
cancel()
|
||||
}()
|
||||
|
||||
log.Infof("log reader started for container: %s", l.id)
|
||||
return logCh
|
||||
}
|
||||
|
||||
func (l *DockerLogs) Stop() { l.done <- true }
|
||||
|
||||
func (l *DockerLogs) parseTime(s string) time.Time {
|
||||
ts, err := time.Parse("2006-01-02T15:04:05.000000000Z", s)
|
||||
if err == nil {
|
||||
return ts
|
||||
}
|
||||
|
||||
ts, err2 := time.Parse("2006-01-02T15:04:05.000000000Z", l.stripPfx(s))
|
||||
if err2 == nil {
|
||||
return ts
|
||||
}
|
||||
|
||||
log.Errorf("failed to parse container log: %s", err)
|
||||
log.Errorf("failed to parse container log2: %s", err2)
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
// attempt to strip message header prefix from a given raw docker log string
|
||||
func (l *DockerLogs) stripPfx(s string) string {
|
||||
b := []byte(s)
|
||||
if len(b) > 8 {
|
||||
return string(b[8:])
|
||||
}
|
||||
return s
|
||||
}
|
||||
35
connector/collector/main.go
Normal file
35
connector/collector/main.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/bcicen/ctop/logging"
|
||||
"github.com/bcicen/ctop/models"
|
||||
)
|
||||
|
||||
var log = logging.Init()
|
||||
|
||||
type LogCollector interface {
|
||||
Stream() chan models.Log
|
||||
Stop()
|
||||
}
|
||||
|
||||
type Collector interface {
|
||||
Stream() chan models.Metrics
|
||||
Logs() LogCollector
|
||||
Running() bool
|
||||
Start()
|
||||
Stop()
|
||||
}
|
||||
|
||||
func round(num float64) int {
|
||||
return int(num + math.Copysign(0.5, num))
|
||||
}
|
||||
|
||||
// return rounded percentage
|
||||
func percent(val float64, total float64) int {
|
||||
if total <= 0 {
|
||||
return 0
|
||||
}
|
||||
return round((val / total) * 100)
|
||||
}
|
||||
@@ -1,16 +1,18 @@
|
||||
// +build !release
|
||||
|
||||
package metrics
|
||||
package collector
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/bcicen/ctop/models"
|
||||
)
|
||||
|
||||
// Mock collector
|
||||
type Mock struct {
|
||||
Metrics
|
||||
stream chan Metrics
|
||||
models.Metrics
|
||||
stream chan models.Metrics
|
||||
done bool
|
||||
running bool
|
||||
aggression int64
|
||||
@@ -18,7 +20,7 @@ type Mock struct {
|
||||
|
||||
func NewMock(a int64) *Mock {
|
||||
c := &Mock{
|
||||
Metrics: Metrics{},
|
||||
Metrics: models.Metrics{},
|
||||
aggression: a,
|
||||
}
|
||||
c.MemLimit = 2147483648
|
||||
@@ -31,7 +33,7 @@ func (c *Mock) Running() bool {
|
||||
|
||||
func (c *Mock) Start() {
|
||||
c.done = false
|
||||
c.stream = make(chan Metrics)
|
||||
c.stream = make(chan models.Metrics)
|
||||
go c.run()
|
||||
}
|
||||
|
||||
@@ -39,15 +41,24 @@ func (c *Mock) Stop() {
|
||||
c.done = true
|
||||
}
|
||||
|
||||
func (c *Mock) Stream() chan Metrics {
|
||||
func (c *Mock) Stream() chan models.Metrics {
|
||||
return c.stream
|
||||
}
|
||||
|
||||
func (c *Mock) Logs() LogCollector {
|
||||
return &MockLogs{make(chan bool)}
|
||||
}
|
||||
|
||||
func (c *Mock) run() {
|
||||
c.running = true
|
||||
rand.Seed(int64(time.Now().Nanosecond()))
|
||||
defer close(c.stream)
|
||||
|
||||
// set to random static value, once
|
||||
c.Pids = rand.Intn(12)
|
||||
c.IOBytesRead = rand.Int63n(8098) * c.aggression
|
||||
c.IOBytesWrite = rand.Int63n(8098) * c.aggression
|
||||
|
||||
for {
|
||||
c.CPUUtil += rand.Intn(2) * int(c.aggression)
|
||||
if c.CPUUtil >= 100 {
|
||||
@@ -60,7 +71,7 @@ func (c *Mock) run() {
|
||||
if c.MemUsage > c.MemLimit {
|
||||
c.MemUsage = 0
|
||||
}
|
||||
c.MemPercent = round((float64(c.MemUsage) / float64(c.MemLimit)) * 100)
|
||||
c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
|
||||
c.stream <- c.Metrics
|
||||
if c.done {
|
||||
break
|
||||
31
connector/collector/mock_logs.go
Normal file
31
connector/collector/mock_logs.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package collector
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/bcicen/ctop/models"
|
||||
)
|
||||
|
||||
const mockLog = "Cura ob pro qui tibi inveni dum qua fit donec amare illic mea, regem falli contexo pro peregrinorum heremo absconditi araneae meminerim deliciosas actionibus facere modico dura sonuerunt psalmi contra rerum, tempus mala anima volebant dura quae o modis."
|
||||
|
||||
type MockLogs struct {
|
||||
done chan bool
|
||||
}
|
||||
|
||||
func (l *MockLogs) Stream() chan models.Log {
|
||||
logCh := make(chan models.Log)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-l.done:
|
||||
break
|
||||
default:
|
||||
logCh <- models.Log{Timestamp: time.Now(), Message: mockLog}
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}()
|
||||
return logCh
|
||||
}
|
||||
|
||||
func (l *MockLogs) Stop() { l.done <- true }
|
||||
45
connector/collector/proc.go
Normal file
45
connector/collector/proc.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// +build linux
|
||||
|
||||
package collector
|
||||
|
||||
import (
|
||||
linuxproc "github.com/c9s/goprocinfo/linux"
|
||||
)
|
||||
|
||||
var sysMemTotal = getSysMemTotal()
|
||||
|
||||
const (
|
||||
clockTicksPerSecond uint64 = 100
|
||||
nanoSecondsPerSecond = 1e9
|
||||
)
|
||||
|
||||
func getSysMemTotal() int64 {
|
||||
stat, err := linuxproc.ReadMemInfo("/proc/meminfo")
|
||||
if err != nil {
|
||||
log.Errorf("error reading system stats: %s", err)
|
||||
return 0
|
||||
}
|
||||
return int64(stat.MemTotal * 1024)
|
||||
}
|
||||
|
||||
// return cumulative system cpu usage in nanoseconds
|
||||
func getSysCPUUsage() uint64 {
|
||||
stat, err := linuxproc.ReadStat("/proc/stat")
|
||||
if err != nil {
|
||||
log.Errorf("error reading system stats: %s", err)
|
||||
return 0
|
||||
}
|
||||
|
||||
sum := stat.CPUStatAll.User +
|
||||
stat.CPUStatAll.Nice +
|
||||
stat.CPUStatAll.System +
|
||||
stat.CPUStatAll.Idle +
|
||||
stat.CPUStatAll.IOWait +
|
||||
stat.CPUStatAll.IRQ +
|
||||
stat.CPUStatAll.SoftIRQ +
|
||||
stat.CPUStatAll.Steal +
|
||||
stat.CPUStatAll.Guest +
|
||||
stat.CPUStatAll.GuestNice
|
||||
|
||||
return (sum * nanoSecondsPerSecond) / clockTicksPerSecond
|
||||
}
|
||||
137
connector/collector/runc.go
Normal file
137
connector/collector/runc.go
Normal file
@@ -0,0 +1,137 @@
|
||||
// +build linux
|
||||
|
||||
package collector
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/opencontainers/runc/libcontainer"
|
||||
"github.com/opencontainers/runc/libcontainer/cgroups"
|
||||
"github.com/opencontainers/runc/types"
|
||||
|
||||
"github.com/bcicen/ctop/config"
|
||||
"github.com/bcicen/ctop/models"
|
||||
)
|
||||
|
||||
// Runc collector
|
||||
type Runc struct {
|
||||
models.Metrics
|
||||
id string
|
||||
libc libcontainer.Container
|
||||
stream chan models.Metrics
|
||||
done bool
|
||||
running bool
|
||||
interval int // collection interval, in seconds
|
||||
lastCpu float64
|
||||
lastSysCpu float64
|
||||
scaleCpu bool
|
||||
}
|
||||
|
||||
func NewRunc(libc libcontainer.Container) *Runc {
|
||||
c := &Runc{
|
||||
Metrics: models.Metrics{},
|
||||
id: libc.ID(),
|
||||
libc: libc,
|
||||
interval: 1,
|
||||
scaleCpu: config.GetSwitchVal("scaleCpu"),
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Runc) Running() bool {
|
||||
return c.running
|
||||
}
|
||||
|
||||
func (c *Runc) Start() {
|
||||
c.done = false
|
||||
c.stream = make(chan models.Metrics)
|
||||
go c.run()
|
||||
}
|
||||
|
||||
func (c *Runc) Stop() {
|
||||
c.done = true
|
||||
}
|
||||
|
||||
func (c *Runc) Stream() chan models.Metrics {
|
||||
return c.stream
|
||||
}
|
||||
|
||||
func (c *Runc) Logs() LogCollector {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Runc) run() {
|
||||
c.running = true
|
||||
defer close(c.stream)
|
||||
log.Debugf("collector started for container: %s", c.id)
|
||||
|
||||
for {
|
||||
stats, err := c.libc.Stats()
|
||||
if err != nil {
|
||||
log.Errorf("failed to collect stats for container %s:\n%s", c.id, err)
|
||||
break
|
||||
}
|
||||
|
||||
c.ReadCPU(stats.CgroupStats)
|
||||
c.ReadMem(stats.CgroupStats)
|
||||
c.ReadNet(stats.Interfaces)
|
||||
|
||||
c.stream <- c.Metrics
|
||||
if c.done {
|
||||
break
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
c.running = false
|
||||
}
|
||||
|
||||
func (c *Runc) ReadCPU(stats *cgroups.Stats) {
|
||||
u := stats.CpuStats.CpuUsage
|
||||
ncpus := float64(len(u.PercpuUsage))
|
||||
total := float64(u.TotalUsage)
|
||||
system := float64(getSysCPUUsage())
|
||||
|
||||
cpudiff := total - c.lastCpu
|
||||
syscpudiff := system - c.lastSysCpu
|
||||
|
||||
if c.scaleCpu {
|
||||
c.CPUUtil = round((cpudiff / syscpudiff * 100))
|
||||
} else {
|
||||
c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
|
||||
}
|
||||
c.lastCpu = total
|
||||
c.lastSysCpu = system
|
||||
c.Pids = int(stats.PidsStats.Current)
|
||||
}
|
||||
|
||||
func (c *Runc) ReadMem(stats *cgroups.Stats) {
|
||||
c.MemUsage = int64(stats.MemoryStats.Usage.Usage)
|
||||
c.MemLimit = int64(stats.MemoryStats.Usage.Limit)
|
||||
if c.MemLimit > sysMemTotal && sysMemTotal > 0 {
|
||||
c.MemLimit = sysMemTotal
|
||||
}
|
||||
c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
|
||||
}
|
||||
|
||||
func (c *Runc) ReadNet(interfaces []*types.NetworkInterface) {
|
||||
var rx, tx int64
|
||||
for _, network := range interfaces {
|
||||
rx += int64(network.RxBytes)
|
||||
tx += int64(network.TxBytes)
|
||||
}
|
||||
c.NetRx, c.NetTx = rx, tx
|
||||
}
|
||||
|
||||
func (c *Runc) ReadIO(stats *cgroups.Stats) {
|
||||
var read, write int64
|
||||
for _, blk := range stats.BlkioStats.IoServiceBytesRecursive {
|
||||
if blk.Op == "Read" {
|
||||
read = int64(blk.Value)
|
||||
}
|
||||
if blk.Op == "Write" {
|
||||
write = int64(blk.Value)
|
||||
}
|
||||
}
|
||||
c.IOBytesRead, c.IOBytesWrite = read, write
|
||||
}
|
||||
222
connector/docker.go
Normal file
222
connector/docker.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package connector
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/bcicen/ctop/connector/collector"
|
||||
"github.com/bcicen/ctop/connector/manager"
|
||||
"github.com/bcicen/ctop/container"
|
||||
api "github.com/fsouza/go-dockerclient"
|
||||
)
|
||||
|
||||
func init() { enabled["docker"] = NewDocker }
|
||||
|
||||
type Docker struct {
|
||||
client *api.Client
|
||||
containers map[string]*container.Container
|
||||
needsRefresh chan string // container IDs requiring refresh
|
||||
closed chan struct{}
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func NewDocker() (Connector, error) {
|
||||
// init docker client
|
||||
client, err := api.NewClientFromEnv()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cm := &Docker{
|
||||
client: client,
|
||||
containers: make(map[string]*container.Container),
|
||||
needsRefresh: make(chan string, 60),
|
||||
closed: make(chan struct{}),
|
||||
lock: sync.RWMutex{},
|
||||
}
|
||||
|
||||
// query info as pre-flight healthcheck
|
||||
info, err := client.Info()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Debugf("docker-connector ID: %s", info.ID)
|
||||
log.Debugf("docker-connector Driver: %s", info.Driver)
|
||||
log.Debugf("docker-connector Images: %d", info.Images)
|
||||
log.Debugf("docker-connector Name: %s", info.Name)
|
||||
log.Debugf("docker-connector ServerVersion: %s", info.ServerVersion)
|
||||
|
||||
go cm.Loop()
|
||||
cm.refreshAll()
|
||||
go cm.watchEvents()
|
||||
return cm, nil
|
||||
}
|
||||
|
||||
// Docker implements Connector
|
||||
func (cm *Docker) Wait() struct{} { return <-cm.closed }
|
||||
|
||||
// Docker events watcher
|
||||
func (cm *Docker) watchEvents() {
|
||||
log.Info("docker event listener starting")
|
||||
events := make(chan *api.APIEvents)
|
||||
cm.client.AddEventListener(events)
|
||||
|
||||
for e := range events {
|
||||
if e.Type != "container" {
|
||||
continue
|
||||
}
|
||||
|
||||
actionName := strings.Split(e.Action, ":")[0]
|
||||
|
||||
switch actionName {
|
||||
case "start", "die", "pause", "unpause", "health_status":
|
||||
log.Debugf("handling docker event: action=%s id=%s", e.Action, e.ID)
|
||||
cm.needsRefresh <- e.ID
|
||||
case "destroy":
|
||||
log.Debugf("handling docker event: action=%s id=%s", e.Action, e.ID)
|
||||
cm.delByID(e.ID)
|
||||
}
|
||||
}
|
||||
log.Info("docker event listener exited")
|
||||
close(cm.closed)
|
||||
}
|
||||
|
||||
func portsFormat(ports map[api.Port][]api.PortBinding) string {
|
||||
var exposed []string
|
||||
var published []string
|
||||
|
||||
for k, v := range ports {
|
||||
if len(v) == 0 {
|
||||
exposed = append(exposed, string(k))
|
||||
continue
|
||||
}
|
||||
for _, binding := range v {
|
||||
s := fmt.Sprintf("%s:%s -> %s", binding.HostIP, binding.HostPort, k)
|
||||
published = append(published, s)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(append(exposed, published...), "\n")
|
||||
}
|
||||
|
||||
func ipsFormat(networks map[string]api.ContainerNetwork) string {
|
||||
var ips []string
|
||||
|
||||
for k, v := range networks {
|
||||
s := fmt.Sprintf("%s:%s", k, v.IPAddress)
|
||||
ips = append(ips, s)
|
||||
}
|
||||
|
||||
return strings.Join(ips, "\n")
|
||||
}
|
||||
|
||||
func (cm *Docker) refresh(c *container.Container) {
|
||||
insp := cm.inspect(c.Id)
|
||||
// remove container if no longer exists
|
||||
if insp == nil {
|
||||
cm.delByID(c.Id)
|
||||
return
|
||||
}
|
||||
c.SetMeta("name", shortName(insp.Name))
|
||||
c.SetMeta("image", insp.Config.Image)
|
||||
c.SetMeta("IPs", ipsFormat(insp.NetworkSettings.Networks))
|
||||
c.SetMeta("ports", portsFormat(insp.NetworkSettings.Ports))
|
||||
c.SetMeta("created", insp.Created.Format("Mon Jan 2 15:04:05 2006"))
|
||||
c.SetMeta("health", insp.State.Health.Status)
|
||||
for _, env := range insp.Config.Env {
|
||||
c.SetMeta("[ENV-VAR]", env)
|
||||
}
|
||||
c.SetState(insp.State.Status)
|
||||
}
|
||||
|
||||
func (cm *Docker) inspect(id string) *api.Container {
|
||||
c, err := cm.client.InspectContainer(id)
|
||||
if err != nil {
|
||||
if _, ok := err.(*api.NoSuchContainer); !ok {
|
||||
log.Errorf("%s (%T)", err.Error(), err)
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Mark all container IDs for refresh
|
||||
func (cm *Docker) refreshAll() {
|
||||
opts := api.ListContainersOptions{All: true}
|
||||
allContainers, err := cm.client.ListContainers(opts)
|
||||
if err != nil {
|
||||
log.Errorf("%s (%T)", err.Error(), err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, i := range allContainers {
|
||||
c := cm.MustGet(i.ID)
|
||||
c.SetMeta("name", shortName(i.Names[0]))
|
||||
c.SetState(i.State)
|
||||
cm.needsRefresh <- c.Id
|
||||
}
|
||||
}
|
||||
|
||||
func (cm *Docker) Loop() {
|
||||
for {
|
||||
select {
|
||||
case id := <-cm.needsRefresh:
|
||||
c := cm.MustGet(id)
|
||||
cm.refresh(c)
|
||||
case <-cm.closed:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MustGet gets a single container, creating one anew if not existing
|
||||
func (cm *Docker) MustGet(id string) *container.Container {
|
||||
c, ok := cm.Get(id)
|
||||
// append container struct for new containers
|
||||
if !ok {
|
||||
// create collector
|
||||
collector := collector.NewDocker(cm.client, id)
|
||||
// create manager
|
||||
manager := manager.NewDocker(cm.client, id)
|
||||
// create container
|
||||
c = container.New(id, collector, manager)
|
||||
cm.lock.Lock()
|
||||
cm.containers[id] = c
|
||||
cm.lock.Unlock()
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Docker implements Connector
|
||||
func (cm *Docker) Get(id string) (*container.Container, bool) {
|
||||
cm.lock.Lock()
|
||||
c, ok := cm.containers[id]
|
||||
cm.lock.Unlock()
|
||||
return c, ok
|
||||
}
|
||||
|
||||
// Remove containers by ID
|
||||
func (cm *Docker) delByID(id string) {
|
||||
cm.lock.Lock()
|
||||
delete(cm.containers, id)
|
||||
cm.lock.Unlock()
|
||||
log.Infof("removed dead container: %s", id)
|
||||
}
|
||||
|
||||
// Docker implements Connector
|
||||
func (cm *Docker) All() (containers container.Containers) {
|
||||
cm.lock.Lock()
|
||||
for _, c := range cm.containers {
|
||||
containers = append(containers, c)
|
||||
}
|
||||
|
||||
containers.Sort()
|
||||
containers.Filter()
|
||||
cm.lock.Unlock()
|
||||
return containers
|
||||
}
|
||||
|
||||
// use primary container name
|
||||
func shortName(name string) string {
|
||||
return strings.Replace(name, "/", "", 1)
|
||||
}
|
||||
104
connector/main.go
Normal file
104
connector/main.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package connector
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bcicen/ctop/container"
|
||||
"github.com/bcicen/ctop/logging"
|
||||
)
|
||||
|
||||
var (
|
||||
log = logging.Init()
|
||||
enabled = make(map[string]ConnectorFn)
|
||||
)
|
||||
|
||||
type ConnectorFn func() (Connector, error)
|
||||
|
||||
type Connector interface {
|
||||
// All returns a pre-sorted container.Containers of all discovered containers
|
||||
All() container.Containers
|
||||
// Get returns a single container.Container by ID
|
||||
Get(string) (*container.Container, bool)
|
||||
// Wait blocks until the underlying connection is lost
|
||||
Wait() struct{}
|
||||
}
|
||||
|
||||
// ConnectorSuper provides initial connection and retry on failure for
|
||||
// an undlerying Connector type
|
||||
type ConnectorSuper struct {
|
||||
conn Connector
|
||||
connFn ConnectorFn
|
||||
err error
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func NewConnectorSuper(connFn ConnectorFn) *ConnectorSuper {
|
||||
cs := &ConnectorSuper{
|
||||
connFn: connFn,
|
||||
err: fmt.Errorf("connecting..."),
|
||||
}
|
||||
go cs.loop()
|
||||
return cs
|
||||
}
|
||||
|
||||
// Get returns the underlying Connector, or nil and an error
|
||||
// if the Connector is not yet initialized or is disconnected.
|
||||
func (cs *ConnectorSuper) Get() (Connector, error) {
|
||||
cs.lock.RLock()
|
||||
defer cs.lock.RUnlock()
|
||||
if cs.err != nil {
|
||||
return nil, cs.err
|
||||
}
|
||||
return cs.conn, nil
|
||||
}
|
||||
|
||||
func (cs *ConnectorSuper) setError(err error) {
|
||||
cs.lock.Lock()
|
||||
defer cs.lock.Unlock()
|
||||
cs.err = err
|
||||
}
|
||||
|
||||
func (cs *ConnectorSuper) loop() {
|
||||
const interval = 3
|
||||
for {
|
||||
log.Infof("initializing connector")
|
||||
|
||||
conn, err := cs.connFn()
|
||||
if err != nil {
|
||||
cs.setError(err)
|
||||
log.Errorf("failed to initialize connector: %s (%T)", err, err)
|
||||
log.Errorf("retrying in %ds", interval)
|
||||
time.Sleep(interval * time.Second)
|
||||
} else {
|
||||
cs.conn = conn
|
||||
cs.setError(nil)
|
||||
log.Infof("successfully initialized connector")
|
||||
|
||||
// wait until connection closed
|
||||
cs.conn.Wait()
|
||||
cs.setError(fmt.Errorf("attempting to reconnect..."))
|
||||
log.Infof("connector closed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enabled returns names for all enabled connectors on the current platform
|
||||
func Enabled() (a []string) {
|
||||
for k, _ := range enabled {
|
||||
a = append(a, k)
|
||||
}
|
||||
sort.Strings(a)
|
||||
return a
|
||||
}
|
||||
|
||||
// ByName returns a ConnectorSuper for a given name, or error if the connector
|
||||
// does not exists on the current platform
|
||||
func ByName(s string) (*ConnectorSuper, error) {
|
||||
if cfn, ok := enabled[s]; ok {
|
||||
return NewConnectorSuper(cfn), nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid connector type \"%s\"", s)
|
||||
}
|
||||
150
connector/manager/docker.go
Normal file
150
connector/manager/docker.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package manager
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
api "github.com/fsouza/go-dockerclient"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Docker struct {
|
||||
id string
|
||||
client *api.Client
|
||||
}
|
||||
|
||||
func NewDocker(client *api.Client, id string) *Docker {
|
||||
return &Docker{
|
||||
id: id,
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
// Do not allow to close reader (i.e. /dev/stdin which docker client tries to close after command execution)
|
||||
type noClosableReader struct {
|
||||
io.Reader
|
||||
}
|
||||
|
||||
func (w *noClosableReader) Read(p []byte) (n int, err error) {
|
||||
return w.Reader.Read(p)
|
||||
}
|
||||
|
||||
const (
|
||||
STDIN = 0
|
||||
STDOUT = 1
|
||||
STDERR = 2
|
||||
)
|
||||
|
||||
var wrongFrameFormat = errors.New("Wrong frame format")
|
||||
|
||||
// A frame has a Header and a Payload
|
||||
// Header: [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4}
|
||||
// STREAM_TYPE can be:
|
||||
// 0: stdin (is written on stdout)
|
||||
// 1: stdout
|
||||
// 2: stderr
|
||||
// SIZE1, SIZE2, SIZE3, SIZE4 are the four bytes of the uint32 size encoded as big endian.
|
||||
// But we don't use size, because we don't need to find the end of frame.
|
||||
type frameWriter struct {
|
||||
stdout io.Writer
|
||||
stderr io.Writer
|
||||
stdin io.Writer
|
||||
}
|
||||
|
||||
func (w *frameWriter) Write(p []byte) (n int, err error) {
|
||||
// drop initial empty frames
|
||||
if len(p) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if len(p) > 8 {
|
||||
var targetWriter io.Writer
|
||||
switch p[0] {
|
||||
case STDIN:
|
||||
targetWriter = w.stdin
|
||||
break
|
||||
case STDOUT:
|
||||
targetWriter = w.stdout
|
||||
break
|
||||
case STDERR:
|
||||
targetWriter = w.stderr
|
||||
break
|
||||
default:
|
||||
return 0, wrongFrameFormat
|
||||
}
|
||||
|
||||
n, err := targetWriter.Write(p[8:])
|
||||
return n + 8, err
|
||||
}
|
||||
|
||||
return 0, wrongFrameFormat
|
||||
}
|
||||
|
||||
func (dc *Docker) Exec(cmd []string) error {
|
||||
execCmd, err := dc.client.CreateExec(api.CreateExecOptions{
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Cmd: cmd,
|
||||
Container: dc.id,
|
||||
Tty: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dc.client.StartExec(execCmd.ID, api.StartExecOptions{
|
||||
InputStream: &noClosableReader{os.Stdin},
|
||||
OutputStream: &frameWriter{os.Stdout, os.Stderr, os.Stdin},
|
||||
ErrorStream: os.Stderr,
|
||||
RawTerminal: true,
|
||||
})
|
||||
}
|
||||
|
||||
func (dc *Docker) Start() error {
|
||||
c, err := dc.client.InspectContainer(dc.id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot inspect container: %v", err)
|
||||
}
|
||||
|
||||
if err := dc.client.StartContainer(c.ID, c.HostConfig); err != nil {
|
||||
return fmt.Errorf("cannot start container: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dc *Docker) Stop() error {
|
||||
if err := dc.client.StopContainer(dc.id, 3); err != nil {
|
||||
return fmt.Errorf("cannot stop container: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dc *Docker) Remove() error {
|
||||
if err := dc.client.RemoveContainer(api.RemoveContainerOptions{ID: dc.id}); err != nil {
|
||||
return fmt.Errorf("cannot remove container: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dc *Docker) Pause() error {
|
||||
if err := dc.client.PauseContainer(dc.id); err != nil {
|
||||
return fmt.Errorf("cannot pause container: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dc *Docker) Unpause() error {
|
||||
if err := dc.client.UnpauseContainer(dc.id); err != nil {
|
||||
return fmt.Errorf("cannot unpause container: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dc *Docker) Restart() error {
|
||||
if err := dc.client.RestartContainer(dc.id, 3); err != nil {
|
||||
return fmt.Errorf("cannot restart container: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
15
connector/manager/main.go
Normal file
15
connector/manager/main.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package manager
|
||||
|
||||
import "errors"
|
||||
|
||||
var ActionNotImplErr = errors.New("action not implemented")
|
||||
|
||||
type Manager interface {
|
||||
Start() error
|
||||
Stop() error
|
||||
Remove() error
|
||||
Pause() error
|
||||
Unpause() error
|
||||
Restart() error
|
||||
Exec(cmd []string) error
|
||||
}
|
||||
35
connector/manager/mock.go
Normal file
35
connector/manager/mock.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package manager
|
||||
|
||||
type Mock struct{}
|
||||
|
||||
func NewMock() *Mock {
|
||||
return &Mock{}
|
||||
}
|
||||
|
||||
func (m *Mock) Start() error {
|
||||
return ActionNotImplErr
|
||||
}
|
||||
|
||||
func (m *Mock) Stop() error {
|
||||
return ActionNotImplErr
|
||||
}
|
||||
|
||||
func (m *Mock) Remove() error {
|
||||
return ActionNotImplErr
|
||||
}
|
||||
|
||||
func (m *Mock) Pause() error {
|
||||
return ActionNotImplErr
|
||||
}
|
||||
|
||||
func (m *Mock) Unpause() error {
|
||||
return ActionNotImplErr
|
||||
}
|
||||
|
||||
func (m *Mock) Restart() error {
|
||||
return ActionNotImplErr
|
||||
}
|
||||
|
||||
func (m *Mock) Exec(cmd []string) error {
|
||||
return ActionNotImplErr
|
||||
}
|
||||
35
connector/manager/runc.go
Normal file
35
connector/manager/runc.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package manager
|
||||
|
||||
type Runc struct{}
|
||||
|
||||
func NewRunc() *Runc {
|
||||
return &Runc{}
|
||||
}
|
||||
|
||||
func (rc *Runc) Start() error {
|
||||
return ActionNotImplErr
|
||||
}
|
||||
|
||||
func (rc *Runc) Stop() error {
|
||||
return ActionNotImplErr
|
||||
}
|
||||
|
||||
func (rc *Runc) Remove() error {
|
||||
return ActionNotImplErr
|
||||
}
|
||||
|
||||
func (rc *Runc) Pause() error {
|
||||
return ActionNotImplErr
|
||||
}
|
||||
|
||||
func (rc *Runc) Unpause() error {
|
||||
return ActionNotImplErr
|
||||
}
|
||||
|
||||
func (rc *Runc) Restart() error {
|
||||
return ActionNotImplErr
|
||||
}
|
||||
|
||||
func (rc *Runc) Exec(cmd []string) error {
|
||||
return ActionNotImplErr
|
||||
}
|
||||
@@ -1,52 +1,81 @@
|
||||
// +build !release
|
||||
|
||||
package main
|
||||
package connector
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bcicen/ctop/metrics"
|
||||
"github.com/bcicen/ctop/connector/collector"
|
||||
"github.com/bcicen/ctop/connector/manager"
|
||||
"github.com/bcicen/ctop/container"
|
||||
"github.com/jgautheron/codename-generator"
|
||||
"github.com/nu7hatch/gouuid"
|
||||
)
|
||||
|
||||
type MockContainerSource struct {
|
||||
containers Containers
|
||||
func init() { enabled["mock"] = NewMock }
|
||||
|
||||
type Mock struct {
|
||||
containers container.Containers
|
||||
}
|
||||
|
||||
func NewMockContainerSource() *MockContainerSource {
|
||||
cs := &MockContainerSource{}
|
||||
func NewMock() (Connector, error) {
|
||||
cs := &Mock{}
|
||||
go cs.Init()
|
||||
go cs.Loop()
|
||||
return cs
|
||||
return cs, nil
|
||||
}
|
||||
|
||||
// Create Mock containers
|
||||
func (cs *MockContainerSource) Init() {
|
||||
func (cs *Mock) Init() {
|
||||
rand.Seed(int64(time.Now().Nanosecond()))
|
||||
|
||||
for i := 0; i < 4; i++ {
|
||||
cs.makeContainer(3)
|
||||
cs.makeContainer(3, true)
|
||||
}
|
||||
|
||||
for i := 0; i < 16; i++ {
|
||||
cs.makeContainer(1)
|
||||
cs.makeContainer(1, false)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (cs *MockContainerSource) makeContainer(aggression int64) {
|
||||
collector := metrics.NewMock(aggression)
|
||||
c := NewContainer(makeID(), collector)
|
||||
func (cs *Mock) Wait() struct{} {
|
||||
ch := make(chan struct{})
|
||||
go func() {
|
||||
time.Sleep(30 * time.Second)
|
||||
close(ch)
|
||||
}()
|
||||
return <-ch
|
||||
}
|
||||
|
||||
var healthStates = []string{"starting", "healthy", "unhealthy"}
|
||||
|
||||
func (cs *Mock) makeContainer(aggression int64, health bool) {
|
||||
collector := collector.NewMock(aggression)
|
||||
manager := manager.NewMock()
|
||||
c := container.New(makeID(), collector, manager)
|
||||
c.SetMeta("name", makeName())
|
||||
c.SetState(makeState())
|
||||
if health {
|
||||
var i int
|
||||
c.SetMeta("health", healthStates[i])
|
||||
go func() {
|
||||
for {
|
||||
i++
|
||||
if i >= len(healthStates) {
|
||||
i = 0
|
||||
}
|
||||
c.SetMeta("health", healthStates[i])
|
||||
time.Sleep(12 * time.Second)
|
||||
}
|
||||
}()
|
||||
}
|
||||
cs.containers = append(cs.containers, c)
|
||||
}
|
||||
|
||||
func (cs *MockContainerSource) Loop() {
|
||||
func (cs *Mock) Loop() {
|
||||
iter := 0
|
||||
for {
|
||||
// Change state for random container
|
||||
@@ -60,7 +89,7 @@ func (cs *MockContainerSource) Loop() {
|
||||
}
|
||||
|
||||
// Get a single container, by ID
|
||||
func (cs *MockContainerSource) Get(id string) (*Container, bool) {
|
||||
func (cs *Mock) Get(id string) (*container.Container, bool) {
|
||||
for _, c := range cs.containers {
|
||||
if c.Id == id {
|
||||
return c, true
|
||||
@@ -69,15 +98,15 @@ func (cs *MockContainerSource) Get(id string) (*Container, bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Return array of all containers, sorted by field
|
||||
func (cs *MockContainerSource) All() Containers {
|
||||
sort.Sort(cs.containers)
|
||||
// All returns array of all containers, sorted by field
|
||||
func (cs *Mock) All() container.Containers {
|
||||
cs.containers.Sort()
|
||||
cs.containers.Filter()
|
||||
return cs.containers
|
||||
}
|
||||
|
||||
// Remove containers by ID
|
||||
func (cs *MockContainerSource) delByID(id string) {
|
||||
func (cs *Mock) delByID(id string) {
|
||||
for n, c := range cs.containers {
|
||||
if c.Id == id {
|
||||
cs.del(n)
|
||||
@@ -87,7 +116,7 @@ func (cs *MockContainerSource) delByID(id string) {
|
||||
}
|
||||
|
||||
// Remove one or more containers by index
|
||||
func (cs *MockContainerSource) del(idx ...int) {
|
||||
func (cs *Mock) del(idx ...int) {
|
||||
for _, i := range idx {
|
||||
cs.containers = append(cs.containers[:i], cs.containers[i+1:]...)
|
||||
}
|
||||
257
connector/runc.go
Normal file
257
connector/runc.go
Normal file
@@ -0,0 +1,257 @@
|
||||
// +build linux
|
||||
|
||||
package connector
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bcicen/ctop/connector/collector"
|
||||
"github.com/bcicen/ctop/connector/manager"
|
||||
"github.com/bcicen/ctop/container"
|
||||
"github.com/opencontainers/runc/libcontainer"
|
||||
"github.com/opencontainers/runc/libcontainer/cgroups/systemd"
|
||||
)
|
||||
|
||||
func init() { enabled["runc"] = NewRunc }
|
||||
|
||||
type RuncOpts struct {
|
||||
root string // runc root path
|
||||
systemdCgroups bool // use systemd cgroups
|
||||
}
|
||||
|
||||
func NewRuncOpts() (RuncOpts, error) {
|
||||
var opts RuncOpts
|
||||
// read runc root path
|
||||
root := os.Getenv("RUNC_ROOT")
|
||||
if root == "" {
|
||||
root = "/run/runc"
|
||||
}
|
||||
abs, err := filepath.Abs(root)
|
||||
if err != nil {
|
||||
return opts, err
|
||||
}
|
||||
opts.root = abs
|
||||
|
||||
// ensure runc root path is readable
|
||||
_, err = ioutil.ReadDir(opts.root)
|
||||
if err != nil {
|
||||
return opts, err
|
||||
}
|
||||
|
||||
if os.Getenv("RUNC_SYSTEMD_CGROUP") == "1" {
|
||||
opts.systemdCgroups = true
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
type Runc struct {
|
||||
opts RuncOpts
|
||||
factory libcontainer.Factory
|
||||
containers map[string]*container.Container
|
||||
libContainers map[string]libcontainer.Container
|
||||
closed chan struct{}
|
||||
needsRefresh chan string // container IDs requiring refresh
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func NewRunc() (Connector, error) {
|
||||
opts, err := NewRuncOpts()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
factory, err := getFactory(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cm := &Runc{
|
||||
opts: opts,
|
||||
factory: factory,
|
||||
containers: make(map[string]*container.Container),
|
||||
libContainers: make(map[string]libcontainer.Container),
|
||||
closed: make(chan struct{}),
|
||||
lock: sync.RWMutex{},
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-cm.closed:
|
||||
return
|
||||
case <-time.After(5 * time.Second):
|
||||
cm.refreshAll()
|
||||
}
|
||||
}
|
||||
}()
|
||||
go cm.Loop()
|
||||
|
||||
return cm, nil
|
||||
}
|
||||
|
||||
func (cm *Runc) GetLibc(id string) libcontainer.Container {
|
||||
// return previously loaded container
|
||||
libc, ok := cm.libContainers[id]
|
||||
if ok {
|
||||
return libc
|
||||
}
|
||||
// load container
|
||||
libc, err := cm.factory.Load(id)
|
||||
if err != nil {
|
||||
// remove container if no longer exists
|
||||
if lerr, ok := err.(libcontainer.Error); ok && lerr.Code() == libcontainer.ContainerNotExists {
|
||||
cm.delByID(id)
|
||||
} else {
|
||||
log.Warningf("failed to read container: %s\n", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return libc
|
||||
}
|
||||
|
||||
// update a ctop container from libcontainer
|
||||
func (cm *Runc) refresh(id string) {
|
||||
libc := cm.GetLibc(id)
|
||||
if libc == nil {
|
||||
return
|
||||
}
|
||||
c := cm.MustGet(id)
|
||||
|
||||
// remove container if entered destroyed state on last refresh
|
||||
// this gives adequate time for the collector to be shut down
|
||||
if c.GetMeta("state") == "destroyed" {
|
||||
cm.delByID(id)
|
||||
return
|
||||
}
|
||||
|
||||
status, err := libc.Status()
|
||||
if err != nil {
|
||||
log.Warningf("failed to read status for container: %s\n", err)
|
||||
} else {
|
||||
c.SetState(status.String())
|
||||
}
|
||||
|
||||
state, err := libc.State()
|
||||
if err != nil {
|
||||
log.Warningf("failed to read state for container: %s\n", err)
|
||||
} else {
|
||||
c.SetMeta("created", state.BaseState.Created.Format("Mon Jan 2 15:04:05 2006"))
|
||||
}
|
||||
|
||||
conf := libc.Config()
|
||||
c.SetMeta("rootfs", conf.Rootfs)
|
||||
}
|
||||
|
||||
// Read runc root, creating any new containers
|
||||
func (cm *Runc) refreshAll() {
|
||||
list, err := ioutil.ReadDir(cm.opts.root)
|
||||
if err != nil {
|
||||
log.Errorf("%s (%T)", err.Error(), err)
|
||||
close(cm.closed)
|
||||
return
|
||||
}
|
||||
|
||||
for _, i := range list {
|
||||
if i.IsDir() {
|
||||
name := i.Name()
|
||||
// attempt to load
|
||||
libc := cm.GetLibc(name)
|
||||
if libc == nil {
|
||||
continue
|
||||
}
|
||||
_ = cm.MustGet(i.Name()) // ensure container exists
|
||||
}
|
||||
}
|
||||
|
||||
// queue all existing containers for refresh
|
||||
for id, _ := range cm.containers {
|
||||
cm.needsRefresh <- id
|
||||
}
|
||||
log.Debugf("queued %d containers for refresh", len(cm.containers))
|
||||
}
|
||||
|
||||
func (cm *Runc) Loop() {
|
||||
for id := range cm.needsRefresh {
|
||||
cm.refresh(id)
|
||||
}
|
||||
}
|
||||
|
||||
// MustGet gets a single ctop container in the map matching libc container, creating one anew if not existing
|
||||
func (cm *Runc) MustGet(id string) *container.Container {
|
||||
c, ok := cm.Get(id)
|
||||
if !ok {
|
||||
libc := cm.GetLibc(id)
|
||||
|
||||
// create collector
|
||||
collector := collector.NewRunc(libc)
|
||||
|
||||
// create container
|
||||
manager := manager.NewRunc()
|
||||
c = container.New(id, collector, manager)
|
||||
|
||||
name := libc.ID()
|
||||
// set initial metadata
|
||||
if len(name) > 12 {
|
||||
name = name[0:12]
|
||||
}
|
||||
c.SetMeta("name", name)
|
||||
|
||||
// add to map
|
||||
cm.lock.Lock()
|
||||
cm.containers[id] = c
|
||||
cm.libContainers[id] = libc
|
||||
cm.lock.Unlock()
|
||||
log.Debugf("saw new container: %s", id)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// Remove containers by ID
|
||||
func (cm *Runc) delByID(id string) {
|
||||
cm.lock.Lock()
|
||||
delete(cm.containers, id)
|
||||
delete(cm.libContainers, id)
|
||||
cm.lock.Unlock()
|
||||
log.Infof("removed dead container: %s", id)
|
||||
}
|
||||
|
||||
// Runc implements Connector
|
||||
func (cm *Runc) Wait() struct{} { return <-cm.closed }
|
||||
|
||||
// Runc implements Connector
|
||||
func (cm *Runc) Get(id string) (*container.Container, bool) {
|
||||
cm.lock.Lock()
|
||||
defer cm.lock.Unlock()
|
||||
c, ok := cm.containers[id]
|
||||
return c, ok
|
||||
}
|
||||
|
||||
// Runc implements Connector
|
||||
func (cm *Runc) All() (containers container.Containers) {
|
||||
cm.lock.Lock()
|
||||
for _, c := range cm.containers {
|
||||
containers = append(containers, c)
|
||||
}
|
||||
containers.Sort()
|
||||
containers.Filter()
|
||||
cm.lock.Unlock()
|
||||
return containers
|
||||
}
|
||||
|
||||
func getFactory(opts RuncOpts) (libcontainer.Factory, error) {
|
||||
cgroupManager := libcontainer.Cgroupfs
|
||||
if opts.systemdCgroups {
|
||||
if systemd.IsRunningSystemd() {
|
||||
cgroupManager = libcontainer.SystemdCgroups
|
||||
} else {
|
||||
return nil, fmt.Errorf("systemd cgroup enabled, but systemd support for managing cgroups is not available")
|
||||
}
|
||||
}
|
||||
return libcontainer.New(opts.root, cgroupManager)
|
||||
}
|
||||
76
container.go
76
container.go
@@ -1,76 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/bcicen/ctop/cwidgets"
|
||||
"github.com/bcicen/ctop/cwidgets/compact"
|
||||
"github.com/bcicen/ctop/metrics"
|
||||
)
|
||||
|
||||
// Metrics and metadata representing a container
|
||||
type Container struct {
|
||||
metrics.Metrics
|
||||
Id string
|
||||
Meta map[string]string
|
||||
Widgets *compact.Compact
|
||||
updater cwidgets.WidgetUpdater
|
||||
collector metrics.Collector
|
||||
display bool // display this container in compact view
|
||||
}
|
||||
|
||||
func NewContainer(id string, collector metrics.Collector) *Container {
|
||||
widgets := compact.NewCompact(id)
|
||||
return &Container{
|
||||
Metrics: metrics.NewMetrics(),
|
||||
Id: id,
|
||||
Meta: make(map[string]string),
|
||||
Widgets: widgets,
|
||||
updater: widgets,
|
||||
collector: collector,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Container) SetUpdater(u cwidgets.WidgetUpdater) {
|
||||
c.updater = u
|
||||
for k, v := range c.Meta {
|
||||
c.updater.SetMeta(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Container) SetMeta(k, v string) {
|
||||
c.Meta[k] = v
|
||||
c.updater.SetMeta(k, v)
|
||||
}
|
||||
|
||||
func (c *Container) GetMeta(k string) string {
|
||||
if v, ok := c.Meta[k]; ok {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *Container) SetState(s string) {
|
||||
c.SetMeta("state", s)
|
||||
// start collector, if needed
|
||||
if s == "running" && !c.collector.Running() {
|
||||
c.collector.Start()
|
||||
c.Read(c.collector.Stream())
|
||||
}
|
||||
// stop collector, if needed
|
||||
if s != "running" && c.collector.Running() {
|
||||
c.collector.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// Read metric stream, updating widgets
|
||||
func (c *Container) Read(stream chan metrics.Metrics) {
|
||||
go func() {
|
||||
for metrics := range stream {
|
||||
c.Metrics = metrics
|
||||
c.updater.SetMetrics(metrics)
|
||||
}
|
||||
log.Infof("reader stopped for container: %s", c.Id)
|
||||
c.Metrics = metrics.NewMetrics()
|
||||
c.Widgets.Reset()
|
||||
}()
|
||||
log.Infof("reader started for container: %s", c.Id)
|
||||
}
|
||||
160
container/main.go
Normal file
160
container/main.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"github.com/bcicen/ctop/connector/collector"
|
||||
"github.com/bcicen/ctop/connector/manager"
|
||||
"github.com/bcicen/ctop/cwidgets"
|
||||
"github.com/bcicen/ctop/cwidgets/compact"
|
||||
"github.com/bcicen/ctop/logging"
|
||||
"github.com/bcicen/ctop/models"
|
||||
)
|
||||
|
||||
var (
|
||||
log = logging.Init()
|
||||
)
|
||||
|
||||
const (
|
||||
running = "running"
|
||||
)
|
||||
|
||||
// Metrics and metadata representing a container
|
||||
type Container struct {
|
||||
models.Metrics
|
||||
Id string
|
||||
Meta models.Meta
|
||||
Widgets *compact.CompactRow
|
||||
Display bool // display this container in compact view
|
||||
updater cwidgets.WidgetUpdater
|
||||
collector collector.Collector
|
||||
manager manager.Manager
|
||||
}
|
||||
|
||||
func New(id string, collector collector.Collector, manager manager.Manager) *Container {
|
||||
widgets := compact.NewCompactRow()
|
||||
return &Container{
|
||||
Metrics: models.NewMetrics(),
|
||||
Id: id,
|
||||
Meta: models.NewMeta("id", id),
|
||||
Widgets: widgets,
|
||||
updater: widgets,
|
||||
collector: collector,
|
||||
manager: manager,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Container) RecreateWidgets() {
|
||||
c.SetUpdater(cwidgets.NullWidgetUpdater{})
|
||||
c.Widgets = compact.NewCompactRow()
|
||||
c.SetUpdater(c.Widgets)
|
||||
}
|
||||
|
||||
func (c *Container) SetUpdater(u cwidgets.WidgetUpdater) {
|
||||
c.updater = u
|
||||
c.updater.SetMeta(c.Meta)
|
||||
}
|
||||
|
||||
func (c *Container) SetMeta(k, v string) {
|
||||
c.Meta[k] = v
|
||||
c.updater.SetMeta(c.Meta)
|
||||
}
|
||||
|
||||
func (c *Container) GetMeta(k string) string {
|
||||
return c.Meta.Get(k)
|
||||
}
|
||||
|
||||
func (c *Container) SetState(s string) {
|
||||
c.SetMeta("state", s)
|
||||
// start collector, if needed
|
||||
if s == running && !c.collector.Running() {
|
||||
c.collector.Start()
|
||||
c.Read(c.collector.Stream())
|
||||
}
|
||||
// stop collector, if needed
|
||||
if s != running && c.collector.Running() {
|
||||
c.collector.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// Logs returns container log collector
|
||||
func (c *Container) Logs() collector.LogCollector {
|
||||
return c.collector.Logs()
|
||||
}
|
||||
|
||||
// Read metric stream, updating widgets
|
||||
func (c *Container) Read(stream chan models.Metrics) {
|
||||
go func() {
|
||||
for metrics := range stream {
|
||||
c.Metrics = metrics
|
||||
c.updater.SetMetrics(metrics)
|
||||
}
|
||||
log.Infof("reader stopped for container: %s", c.Id)
|
||||
c.Metrics = models.NewMetrics()
|
||||
c.Widgets.Reset()
|
||||
}()
|
||||
log.Infof("reader started for container: %s", c.Id)
|
||||
}
|
||||
|
||||
func (c *Container) Start() {
|
||||
if c.Meta["state"] != running {
|
||||
if err := c.manager.Start(); err != nil {
|
||||
log.Warningf("container %s: %v", c.Id, err)
|
||||
log.StatusErr(err)
|
||||
return
|
||||
}
|
||||
c.SetState(running)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Container) Stop() {
|
||||
if c.Meta["state"] == running {
|
||||
if err := c.manager.Stop(); err != nil {
|
||||
log.Warningf("container %s: %v", c.Id, err)
|
||||
log.StatusErr(err)
|
||||
return
|
||||
}
|
||||
c.SetState("exited")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Container) Remove() {
|
||||
if err := c.manager.Remove(); err != nil {
|
||||
log.Warningf("container %s: %v", c.Id, err)
|
||||
log.StatusErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Container) Pause() {
|
||||
if c.Meta["state"] == running {
|
||||
if err := c.manager.Pause(); err != nil {
|
||||
log.Warningf("container %s: %v", c.Id, err)
|
||||
log.StatusErr(err)
|
||||
return
|
||||
}
|
||||
c.SetState("paused")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Container) Unpause() {
|
||||
if c.Meta["state"] == "paused" {
|
||||
if err := c.manager.Unpause(); err != nil {
|
||||
log.Warningf("container %s: %v", c.Id, err)
|
||||
log.StatusErr(err)
|
||||
return
|
||||
}
|
||||
c.SetState(running)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Container) Restart() {
|
||||
if c.Meta["state"] == running {
|
||||
if err := c.manager.Restart(); err != nil {
|
||||
log.Warningf("container %s: %v", c.Id, err)
|
||||
log.StatusErr(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Container) Exec(cmd []string) error {
|
||||
return c.manager.Exec(cmd)
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
package main
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
|
||||
"github.com/bcicen/ctop/config"
|
||||
)
|
||||
@@ -89,6 +90,7 @@ func SortFields() (fields []string) {
|
||||
|
||||
type Containers []*Container
|
||||
|
||||
func (a Containers) Sort() { sort.Sort(a) }
|
||||
func (a Containers) Len() int { return len(a) }
|
||||
func (a Containers) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a Containers) Less(i, j int) bool {
|
||||
@@ -104,14 +106,14 @@ func (a Containers) Filter() {
|
||||
re := regexp.MustCompile(fmt.Sprintf(".*%s", filter))
|
||||
|
||||
for _, c := range a {
|
||||
c.display = true
|
||||
c.Display = true
|
||||
// Apply name filter
|
||||
if re.FindAllString(c.GetMeta("name"), 1) == nil {
|
||||
c.display = false
|
||||
c.Display = false
|
||||
}
|
||||
// Apply state filter
|
||||
if !config.GetSwitchVal("allContainers") && c.GetMeta("state") != "running" {
|
||||
c.display = false
|
||||
c.Display = false
|
||||
}
|
||||
}
|
||||
}
|
||||
131
cursor.go
131
cursor.go
@@ -1,24 +1,23 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/bcicen/ctop/connector"
|
||||
"github.com/bcicen/ctop/container"
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
type GridCursor struct {
|
||||
selectedID string // id of currently selected container
|
||||
filtered Containers
|
||||
cSource ContainerSource
|
||||
}
|
||||
|
||||
func NewGridCursor() *GridCursor {
|
||||
return &GridCursor{
|
||||
cSource: NewDockerContainerSource(),
|
||||
}
|
||||
selectedID string // id of currently selected container
|
||||
filtered container.Containers
|
||||
cSuper *connector.ConnectorSuper
|
||||
isScrolling bool // toggled when actively scrolling
|
||||
}
|
||||
|
||||
func (gc *GridCursor) Len() int { return len(gc.filtered) }
|
||||
|
||||
func (gc *GridCursor) Selected() *Container {
|
||||
func (gc *GridCursor) Selected() *container.Container {
|
||||
idx := gc.Idx()
|
||||
if idx < gc.Len() {
|
||||
return gc.filtered[idx]
|
||||
@@ -26,15 +25,21 @@ func (gc *GridCursor) Selected() *Container {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Refresh containers from source
|
||||
func (gc *GridCursor) RefreshContainers() (lenChanged bool) {
|
||||
// Refresh containers from source, returning whether the quantity of
|
||||
// containers has changed and any error
|
||||
func (gc *GridCursor) RefreshContainers() (bool, error) {
|
||||
oldLen := gc.Len()
|
||||
gc.filtered = container.Containers{}
|
||||
|
||||
// Containers filtered by display bool
|
||||
gc.filtered = Containers{}
|
||||
cSource, err := gc.cSuper.Get()
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
// filter Containers by display bool
|
||||
var cursorVisible bool
|
||||
for _, c := range gc.cSource.All() {
|
||||
if c.display {
|
||||
for _, c := range cSource.All() {
|
||||
if c.Display {
|
||||
if c.Id == gc.selectedID {
|
||||
cursorVisible = true
|
||||
}
|
||||
@@ -42,31 +47,30 @@ func (gc *GridCursor) RefreshContainers() (lenChanged bool) {
|
||||
}
|
||||
}
|
||||
|
||||
if oldLen != gc.Len() {
|
||||
lenChanged = true
|
||||
if !cursorVisible || gc.selectedID == "" {
|
||||
gc.Reset()
|
||||
}
|
||||
|
||||
if !cursorVisible {
|
||||
gc.Reset()
|
||||
}
|
||||
if gc.selectedID == "" {
|
||||
gc.Reset()
|
||||
}
|
||||
return lenChanged
|
||||
return oldLen != gc.Len(), nil
|
||||
}
|
||||
|
||||
// Set an initial cursor position, if possible
|
||||
func (gc *GridCursor) Reset() {
|
||||
for _, c := range gc.cSource.All() {
|
||||
c.Widgets.Name.UnHighlight()
|
||||
cSource, err := gc.cSuper.Get()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, c := range cSource.All() {
|
||||
c.Widgets.UnHighlight()
|
||||
}
|
||||
if gc.Len() > 0 {
|
||||
gc.selectedID = gc.filtered[0].Id
|
||||
gc.filtered[0].Widgets.Name.Highlight()
|
||||
gc.filtered[0].Widgets.Highlight()
|
||||
}
|
||||
}
|
||||
|
||||
// Return current cursor index
|
||||
// Idx returns current cursor index
|
||||
func (gc *GridCursor) Idx() int {
|
||||
for n, c := range gc.filtered {
|
||||
if c.Id == gc.selectedID {
|
||||
@@ -100,6 +104,9 @@ func (gc *GridCursor) ScrollPage() {
|
||||
}
|
||||
|
||||
func (gc *GridCursor) Up() {
|
||||
gc.isScrolling = true
|
||||
defer func() { gc.isScrolling = false }()
|
||||
|
||||
idx := gc.Idx()
|
||||
if idx <= 0 { // already at top
|
||||
return
|
||||
@@ -107,15 +114,18 @@ func (gc *GridCursor) Up() {
|
||||
active := gc.filtered[idx]
|
||||
next := gc.filtered[idx-1]
|
||||
|
||||
active.Widgets.Name.UnHighlight()
|
||||
active.Widgets.UnHighlight()
|
||||
gc.selectedID = next.Id
|
||||
next.Widgets.Name.Highlight()
|
||||
next.Widgets.Highlight()
|
||||
|
||||
gc.ScrollPage()
|
||||
ui.Render(cGrid)
|
||||
}
|
||||
|
||||
func (gc *GridCursor) Down() {
|
||||
gc.isScrolling = true
|
||||
defer func() { gc.isScrolling = false }()
|
||||
|
||||
idx := gc.Idx()
|
||||
if idx >= gc.Len()-1 { // already at bottom
|
||||
return
|
||||
@@ -123,10 +133,65 @@ func (gc *GridCursor) Down() {
|
||||
active := gc.filtered[idx]
|
||||
next := gc.filtered[idx+1]
|
||||
|
||||
active.Widgets.Name.UnHighlight()
|
||||
active.Widgets.UnHighlight()
|
||||
gc.selectedID = next.Id
|
||||
next.Widgets.Name.Highlight()
|
||||
next.Widgets.Highlight()
|
||||
|
||||
gc.ScrollPage()
|
||||
ui.Render(cGrid)
|
||||
}
|
||||
|
||||
func (gc *GridCursor) PgUp() {
|
||||
idx := gc.Idx()
|
||||
if idx <= 0 { // already at top
|
||||
return
|
||||
}
|
||||
|
||||
nextidx := int(math.Max(0.0, float64(idx-cGrid.MaxRows())))
|
||||
if gc.pgCount() > 0 {
|
||||
cGrid.Offset = int(math.Max(float64(cGrid.Offset-cGrid.MaxRows()),
|
||||
float64(0)))
|
||||
}
|
||||
|
||||
active := gc.filtered[idx]
|
||||
next := gc.filtered[nextidx]
|
||||
|
||||
active.Widgets.UnHighlight()
|
||||
gc.selectedID = next.Id
|
||||
next.Widgets.Highlight()
|
||||
|
||||
cGrid.Align()
|
||||
ui.Render(cGrid)
|
||||
}
|
||||
|
||||
func (gc *GridCursor) PgDown() {
|
||||
idx := gc.Idx()
|
||||
if idx >= gc.Len()-1 { // already at bottom
|
||||
return
|
||||
}
|
||||
|
||||
nextidx := int(math.Min(float64(gc.Len()-1), float64(idx+cGrid.MaxRows())))
|
||||
if gc.pgCount() > 0 {
|
||||
cGrid.Offset = int(math.Min(float64(cGrid.Offset+cGrid.MaxRows()),
|
||||
float64(gc.Len()-cGrid.MaxRows())))
|
||||
}
|
||||
|
||||
active := gc.filtered[idx]
|
||||
next := gc.filtered[nextidx]
|
||||
|
||||
active.Widgets.UnHighlight()
|
||||
gc.selectedID = next.Id
|
||||
next.Widgets.Highlight()
|
||||
|
||||
cGrid.Align()
|
||||
ui.Render(cGrid)
|
||||
}
|
||||
|
||||
// number of pages at current row count and term height
|
||||
func (gc *GridCursor) pgCount() int {
|
||||
pages := gc.Len() / cGrid.MaxRows()
|
||||
if gc.Len()%cGrid.MaxRows() > 0 {
|
||||
pages++
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
49
cwidgets/compact/column.go
Normal file
49
cwidgets/compact/column.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package compact
|
||||
|
||||
import (
|
||||
"github.com/bcicen/ctop/config"
|
||||
"github.com/bcicen/ctop/models"
|
||||
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
var (
|
||||
allCols = map[string]NewCompactColFn{
|
||||
"status": NewStatus,
|
||||
"name": NewNameCol,
|
||||
"id": NewCIDCol,
|
||||
"cpu": NewCPUCol,
|
||||
"mem": NewMemCol,
|
||||
"net": NewNetCol,
|
||||
"io": NewIOCol,
|
||||
"pids": NewPIDCol,
|
||||
}
|
||||
)
|
||||
|
||||
type NewCompactColFn func() CompactCol
|
||||
|
||||
func newRowWidgets() []CompactCol {
|
||||
enabled := config.EnabledColumns()
|
||||
cols := make([]CompactCol, len(enabled))
|
||||
|
||||
for n, name := range enabled {
|
||||
wFn, ok := allCols[name]
|
||||
if !ok {
|
||||
panic("no such widget name: %s" + name)
|
||||
}
|
||||
cols[n] = wFn()
|
||||
}
|
||||
|
||||
return cols
|
||||
}
|
||||
|
||||
type CompactCol interface {
|
||||
ui.GridBufferer
|
||||
Reset()
|
||||
Header() string // header text to display for column
|
||||
FixedWidth() int // fixed width size. if == 0, width is automatically calculated
|
||||
Highlight()
|
||||
UnHighlight()
|
||||
SetMeta(models.Meta)
|
||||
SetMetrics(models.Metrics)
|
||||
}
|
||||
@@ -1,21 +1,60 @@
|
||||
package compact
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/bcicen/ctop/cwidgets"
|
||||
"github.com/bcicen/ctop/models"
|
||||
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
type GaugeCol struct {
|
||||
*ui.Gauge
|
||||
type CPUCol struct {
|
||||
*GaugeCol
|
||||
}
|
||||
|
||||
func NewGaugeCol() *GaugeCol {
|
||||
g := ui.NewGauge()
|
||||
func NewCPUCol() CompactCol {
|
||||
return &CPUCol{NewGaugeCol("CPU")}
|
||||
}
|
||||
|
||||
func (w *CPUCol) SetMetrics(m models.Metrics) {
|
||||
val := m.CPUUtil
|
||||
w.BarColor = colorScale(val)
|
||||
w.Label = fmt.Sprintf("%d%%", val)
|
||||
|
||||
if val > 100 {
|
||||
val = 100
|
||||
}
|
||||
w.Percent = val
|
||||
}
|
||||
|
||||
type MemCol struct {
|
||||
*GaugeCol
|
||||
}
|
||||
|
||||
func NewMemCol() CompactCol {
|
||||
return &MemCol{NewGaugeCol("MEM")}
|
||||
}
|
||||
|
||||
func (w *MemCol) SetMetrics(m models.Metrics) {
|
||||
w.BarColor = ui.ThemeAttr("gauge.bar.bg")
|
||||
w.Label = fmt.Sprintf("%s / %s", cwidgets.ByteFormat64Short(m.MemUsage), cwidgets.ByteFormat64Short(m.MemLimit))
|
||||
w.Percent = m.MemPercent
|
||||
}
|
||||
|
||||
type GaugeCol struct {
|
||||
*ui.Gauge
|
||||
header string
|
||||
fWidth int
|
||||
}
|
||||
|
||||
func NewGaugeCol(header string) *GaugeCol {
|
||||
g := &GaugeCol{ui.NewGauge(), header, 0}
|
||||
g.Height = 1
|
||||
g.Border = false
|
||||
g.Percent = 0
|
||||
g.PaddingBottom = 0
|
||||
g.Label = "-"
|
||||
return &GaugeCol{g}
|
||||
g.Reset()
|
||||
return g
|
||||
}
|
||||
|
||||
func (w *GaugeCol) Reset() {
|
||||
@@ -23,12 +62,41 @@ func (w *GaugeCol) Reset() {
|
||||
w.Percent = 0
|
||||
}
|
||||
|
||||
func (w *GaugeCol) Buffer() ui.Buffer {
|
||||
// if bar would not otherwise be visible, set a minimum
|
||||
// percentage value and low-contrast color for structure
|
||||
if w.Percent < 5 {
|
||||
w.Percent = 5
|
||||
w.BarColor = ui.ColorBlack
|
||||
}
|
||||
|
||||
return w.Gauge.Buffer()
|
||||
}
|
||||
|
||||
// GaugeCol implements CompactCol
|
||||
func (w *GaugeCol) SetMeta(models.Meta) {}
|
||||
func (w *GaugeCol) SetMetrics(models.Metrics) {}
|
||||
func (w *GaugeCol) Header() string { return w.header }
|
||||
func (w *GaugeCol) FixedWidth() int { return w.fWidth }
|
||||
|
||||
// GaugeCol implements CompactCol
|
||||
func (w *GaugeCol) Highlight() {
|
||||
w.Bg = ui.ThemeAttr("par.text.fg")
|
||||
w.PercentColor = ui.ThemeAttr("par.text.hi")
|
||||
}
|
||||
|
||||
// GaugeCol implements CompactCol
|
||||
func (w *GaugeCol) UnHighlight() {
|
||||
w.Bg = ui.ThemeAttr("par.text.bg")
|
||||
w.PercentColor = ui.ThemeAttr("par.text.bg")
|
||||
}
|
||||
|
||||
func colorScale(n int) ui.Attribute {
|
||||
if n > 70 {
|
||||
return ui.ColorRed
|
||||
return ui.ThemeAttr("status.danger")
|
||||
}
|
||||
if n > 30 {
|
||||
return ui.ColorYellow
|
||||
return ui.ThemeAttr("status.warn")
|
||||
}
|
||||
return ui.ColorGreen
|
||||
return ui.ThemeAttr("status.ok")
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ import (
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
var header *CompactHeader
|
||||
|
||||
type CompactGrid struct {
|
||||
ui.GridBufferer
|
||||
Rows []ui.GridBufferer
|
||||
header *CompactHeader
|
||||
cols []CompactCol // reference columns
|
||||
Rows []RowBufferer
|
||||
X, Y int
|
||||
Width int
|
||||
Height int
|
||||
@@ -16,32 +16,64 @@ type CompactGrid struct {
|
||||
}
|
||||
|
||||
func NewCompactGrid() *CompactGrid {
|
||||
header = NewCompactHeader() // init column header
|
||||
return &CompactGrid{}
|
||||
cg := &CompactGrid{header: NewCompactHeader()}
|
||||
cg.rebuildHeader()
|
||||
return cg
|
||||
}
|
||||
|
||||
func (cg *CompactGrid) Align() {
|
||||
y := cg.Y
|
||||
if cg.Offset >= len(cg.Rows) {
|
||||
|
||||
if cg.Offset >= len(cg.Rows) || cg.Offset < 0 {
|
||||
cg.Offset = 0
|
||||
}
|
||||
|
||||
// update row ypos, width recursively
|
||||
colWidths := cg.calcWidths()
|
||||
for _, r := range cg.pageRows() {
|
||||
r.SetY(y)
|
||||
y += r.GetHeight()
|
||||
r.SetWidth(cg.Width)
|
||||
r.SetWidths(cg.Width, colWidths)
|
||||
}
|
||||
}
|
||||
|
||||
func (cg *CompactGrid) Clear() { cg.Rows = []ui.GridBufferer{} }
|
||||
func (cg *CompactGrid) GetHeight() int { return len(cg.Rows) + header.Height }
|
||||
func (cg *CompactGrid) Clear() {
|
||||
cg.Rows = []RowBufferer{}
|
||||
cg.rebuildHeader()
|
||||
}
|
||||
|
||||
func (cg *CompactGrid) GetHeight() int { return len(cg.Rows) + cg.header.Height }
|
||||
func (cg *CompactGrid) SetX(x int) { cg.X = x }
|
||||
func (cg *CompactGrid) SetY(y int) { cg.Y = y }
|
||||
func (cg *CompactGrid) SetWidth(w int) { cg.Width = w }
|
||||
func (cg *CompactGrid) MaxRows() int { return ui.TermHeight() - header.Height - cg.Y }
|
||||
func (cg *CompactGrid) MaxRows() int { return ui.TermHeight() - cg.header.Height - cg.Y }
|
||||
|
||||
func (cg *CompactGrid) pageRows() (rows []ui.GridBufferer) {
|
||||
rows = append(rows, header)
|
||||
// calculate and return per-column width
|
||||
func (cg *CompactGrid) calcWidths() []int {
|
||||
var autoCols int
|
||||
width := cg.Width
|
||||
colWidths := make([]int, len(cg.cols))
|
||||
|
||||
for n, w := range cg.cols {
|
||||
colWidths[n] = w.FixedWidth()
|
||||
width -= w.FixedWidth()
|
||||
if w.FixedWidth() == 0 {
|
||||
autoCols++
|
||||
}
|
||||
}
|
||||
|
||||
spacing := colSpacing * len(cg.cols)
|
||||
autoWidth := (width - spacing) / autoCols
|
||||
for n, val := range colWidths {
|
||||
if val == 0 {
|
||||
colWidths[n] = autoWidth
|
||||
}
|
||||
}
|
||||
return colWidths
|
||||
}
|
||||
|
||||
func (cg *CompactGrid) pageRows() (rows []RowBufferer) {
|
||||
rows = append(rows, cg.header)
|
||||
rows = append(rows, cg.Rows[cg.Offset:]...)
|
||||
return rows
|
||||
}
|
||||
@@ -54,8 +86,14 @@ func (cg *CompactGrid) Buffer() ui.Buffer {
|
||||
return buf
|
||||
}
|
||||
|
||||
func (cg *CompactGrid) AddRows(rows ...ui.GridBufferer) {
|
||||
for _, r := range rows {
|
||||
cg.Rows = append(cg.Rows, r)
|
||||
func (cg *CompactGrid) AddRows(rows ...RowBufferer) {
|
||||
cg.Rows = append(cg.Rows, rows...)
|
||||
}
|
||||
|
||||
func (cg *CompactGrid) rebuildHeader() {
|
||||
cg.cols = newRowWidgets()
|
||||
cg.header.clearFieldPars()
|
||||
for _, col := range cg.cols {
|
||||
cg.header.addFieldPar(col.Header())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,63 +8,59 @@ type CompactHeader struct {
|
||||
X, Y int
|
||||
Width int
|
||||
Height int
|
||||
cols []CompactCol
|
||||
widths []int
|
||||
pars []*ui.Par
|
||||
}
|
||||
|
||||
func NewCompactHeader() *CompactHeader {
|
||||
fields := []string{"", "NAME", "CID", "CPU", "MEM", "NET RX/TX", "IO R/W", "PIDS"}
|
||||
ch := &CompactHeader{}
|
||||
ch.Height = 2
|
||||
for _, f := range fields {
|
||||
ch.addFieldPar(f)
|
||||
return &CompactHeader{
|
||||
X: rowPadding,
|
||||
Height: 2,
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
func (ch *CompactHeader) GetHeight() int {
|
||||
return ch.Height
|
||||
func (row *CompactHeader) GetHeight() int {
|
||||
return row.Height
|
||||
}
|
||||
|
||||
func (ch *CompactHeader) SetWidth(w int) {
|
||||
x := ch.X
|
||||
autoWidth := calcWidth(w)
|
||||
for n, col := range ch.pars {
|
||||
// set column to static width
|
||||
if colWidths[n] != 0 {
|
||||
col.SetX(x)
|
||||
col.SetWidth(colWidths[n])
|
||||
x += colWidths[n]
|
||||
continue
|
||||
}
|
||||
col.SetX(x)
|
||||
col.SetWidth(autoWidth)
|
||||
x += autoWidth + colSpacing
|
||||
func (row *CompactHeader) SetWidths(totalWidth int, widths []int) {
|
||||
x := row.X
|
||||
|
||||
for n, w := range row.pars {
|
||||
w.SetX(x)
|
||||
w.SetWidth(widths[n])
|
||||
x += widths[n] + colSpacing
|
||||
}
|
||||
ch.Width = w
|
||||
row.Width = totalWidth
|
||||
}
|
||||
|
||||
func (ch *CompactHeader) SetX(x int) {
|
||||
ch.X = x
|
||||
func (row *CompactHeader) SetX(x int) {
|
||||
row.X = x
|
||||
}
|
||||
|
||||
func (ch *CompactHeader) SetY(y int) {
|
||||
for _, p := range ch.pars {
|
||||
func (row *CompactHeader) SetY(y int) {
|
||||
for _, p := range row.pars {
|
||||
p.SetY(y)
|
||||
}
|
||||
ch.Y = y
|
||||
row.Y = y
|
||||
}
|
||||
|
||||
func (ch *CompactHeader) Buffer() ui.Buffer {
|
||||
func (row *CompactHeader) Buffer() ui.Buffer {
|
||||
buf := ui.NewBuffer()
|
||||
for _, p := range ch.pars {
|
||||
for _, p := range row.pars {
|
||||
buf.Merge(p.Buffer())
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
func (ch *CompactHeader) addFieldPar(s string) {
|
||||
p := ui.NewPar(s)
|
||||
p.Height = ch.Height
|
||||
p.Border = false
|
||||
ch.pars = append(ch.pars, p)
|
||||
func (row *CompactHeader) clearFieldPars() {
|
||||
row.pars = []*ui.Par{}
|
||||
}
|
||||
|
||||
func (row *CompactHeader) addFieldPar(s string) {
|
||||
p := ui.NewPar(s)
|
||||
p.Height = row.Height
|
||||
p.Border = false
|
||||
row.pars = append(row.pars, p)
|
||||
}
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
package compact
|
||||
|
||||
import (
|
||||
"github.com/bcicen/ctop/logging"
|
||||
"github.com/bcicen/ctop/metrics"
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
var log = logging.Init()
|
||||
|
||||
type Compact struct {
|
||||
Status *Status
|
||||
Name *TextCol
|
||||
Cid *TextCol
|
||||
Cpu *GaugeCol
|
||||
Memory *GaugeCol
|
||||
Net *TextCol
|
||||
IO *TextCol
|
||||
Pids *TextCol
|
||||
X, Y int
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
func NewCompact(id string) *Compact {
|
||||
// truncate container id
|
||||
if len(id) > 12 {
|
||||
id = id[:12]
|
||||
}
|
||||
row := &Compact{
|
||||
Status: NewStatus(),
|
||||
Name: NewTextCol("-"),
|
||||
Cid: NewTextCol(id),
|
||||
Cpu: NewGaugeCol(),
|
||||
Memory: NewGaugeCol(),
|
||||
Net: NewTextCol("-"),
|
||||
IO: NewTextCol("-"),
|
||||
Pids: NewTextCol("-"),
|
||||
X: 1,
|
||||
Height: 1,
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
//func (row *Compact) ToggleExpand() {
|
||||
//if row.Height == 1 {
|
||||
//row.Height = 4
|
||||
//} else {
|
||||
//row.Height = 1
|
||||
//}
|
||||
//}
|
||||
|
||||
func (row *Compact) SetMeta(k, v string) {
|
||||
switch k {
|
||||
case "name":
|
||||
row.Name.Set(v)
|
||||
case "state":
|
||||
row.Status.Set(v)
|
||||
}
|
||||
}
|
||||
|
||||
func (row *Compact) SetMetrics(m metrics.Metrics) {
|
||||
row.SetCPU(m.CPUUtil)
|
||||
row.SetNet(m.NetRx, m.NetTx)
|
||||
row.SetMem(m.MemUsage, m.MemLimit, m.MemPercent)
|
||||
row.SetIO(m.IOBytesRead, m.IOBytesWrite)
|
||||
row.SetPids(m.Pids)
|
||||
}
|
||||
|
||||
// Set gauges, counters to default unread values
|
||||
func (row *Compact) Reset() {
|
||||
row.Cpu.Reset()
|
||||
row.Memory.Reset()
|
||||
row.Net.Reset()
|
||||
row.IO.Reset()
|
||||
row.Pids.Reset()
|
||||
}
|
||||
|
||||
func (row *Compact) GetHeight() int {
|
||||
return row.Height
|
||||
}
|
||||
|
||||
func (row *Compact) SetX(x int) {
|
||||
row.X = x
|
||||
}
|
||||
|
||||
func (row *Compact) SetY(y int) {
|
||||
if y == row.Y {
|
||||
return
|
||||
}
|
||||
for _, col := range row.all() {
|
||||
col.SetY(y)
|
||||
}
|
||||
row.Y = y
|
||||
}
|
||||
|
||||
func (row *Compact) SetWidth(width int) {
|
||||
if width == row.Width {
|
||||
return
|
||||
}
|
||||
x := row.X
|
||||
autoWidth := calcWidth(width)
|
||||
for n, col := range row.all() {
|
||||
if colWidths[n] != 0 {
|
||||
col.SetX(x)
|
||||
col.SetWidth(colWidths[n])
|
||||
x += colWidths[n]
|
||||
continue
|
||||
}
|
||||
col.SetX(x)
|
||||
col.SetWidth(autoWidth)
|
||||
x += autoWidth + colSpacing
|
||||
}
|
||||
row.Width = width
|
||||
}
|
||||
|
||||
func (row *Compact) Buffer() ui.Buffer {
|
||||
buf := ui.NewBuffer()
|
||||
|
||||
buf.Merge(row.Status.Buffer())
|
||||
buf.Merge(row.Name.Buffer())
|
||||
buf.Merge(row.Cid.Buffer())
|
||||
buf.Merge(row.Cpu.Buffer())
|
||||
buf.Merge(row.Memory.Buffer())
|
||||
buf.Merge(row.Net.Buffer())
|
||||
buf.Merge(row.IO.Buffer())
|
||||
buf.Merge(row.Pids.Buffer())
|
||||
return buf
|
||||
}
|
||||
|
||||
func (row *Compact) all() []ui.GridBufferer {
|
||||
return []ui.GridBufferer{
|
||||
row.Status,
|
||||
row.Name,
|
||||
row.Cid,
|
||||
row.Cpu,
|
||||
row.Memory,
|
||||
row.Net,
|
||||
row.IO,
|
||||
row.Pids,
|
||||
}
|
||||
}
|
||||
129
cwidgets/compact/row.go
Normal file
129
cwidgets/compact/row.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package compact
|
||||
|
||||
import (
|
||||
"github.com/bcicen/ctop/config"
|
||||
"github.com/bcicen/ctop/logging"
|
||||
"github.com/bcicen/ctop/models"
|
||||
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
const rowPadding = 1
|
||||
|
||||
var log = logging.Init()
|
||||
|
||||
type RowBufferer interface {
|
||||
SetY(int)
|
||||
SetWidths(int, []int)
|
||||
GetHeight() int
|
||||
Buffer() ui.Buffer
|
||||
}
|
||||
|
||||
type CompactRow struct {
|
||||
Bg *RowBg
|
||||
Cols []CompactCol
|
||||
X, Y int
|
||||
Height int
|
||||
widths []int // column widths
|
||||
}
|
||||
|
||||
func NewCompactRow() *CompactRow {
|
||||
row := &CompactRow{
|
||||
Bg: NewRowBg(),
|
||||
Cols: newRowWidgets(),
|
||||
X: rowPadding,
|
||||
Height: 1,
|
||||
}
|
||||
|
||||
return row
|
||||
}
|
||||
|
||||
func (row *CompactRow) SetMeta(m models.Meta) {
|
||||
for _, w := range row.Cols {
|
||||
w.SetMeta(m)
|
||||
}
|
||||
}
|
||||
|
||||
func (row *CompactRow) SetMetrics(m models.Metrics) {
|
||||
for _, w := range row.Cols {
|
||||
w.SetMetrics(m)
|
||||
}
|
||||
}
|
||||
|
||||
// Set gauges, counters, etc. to default unread values
|
||||
func (row *CompactRow) Reset() {
|
||||
for _, w := range row.Cols {
|
||||
w.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
func (row *CompactRow) GetHeight() int { return row.Height }
|
||||
|
||||
//func (row *CompactRow) SetX(x int) { row.X = x }
|
||||
|
||||
func (row *CompactRow) SetY(y int) {
|
||||
if y == row.Y {
|
||||
return
|
||||
}
|
||||
|
||||
row.Bg.Y = y
|
||||
for _, w := range row.Cols {
|
||||
w.SetY(y)
|
||||
}
|
||||
row.Y = y
|
||||
}
|
||||
|
||||
func (row *CompactRow) SetWidths(totalWidth int, widths []int) {
|
||||
x := row.X
|
||||
|
||||
row.Bg.SetX(x)
|
||||
row.Bg.SetWidth(totalWidth)
|
||||
|
||||
for n, w := range row.Cols {
|
||||
w.SetX(x)
|
||||
w.SetWidth(widths[n])
|
||||
x += widths[n] + colSpacing
|
||||
}
|
||||
}
|
||||
|
||||
func (row *CompactRow) Buffer() ui.Buffer {
|
||||
buf := ui.NewBuffer()
|
||||
buf.Merge(row.Bg.Buffer())
|
||||
for _, w := range row.Cols {
|
||||
buf.Merge(w.Buffer())
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
func (row *CompactRow) Highlight() {
|
||||
row.Cols[1].Highlight()
|
||||
if config.GetSwitchVal("fullRowCursor") {
|
||||
for _, w := range row.Cols {
|
||||
w.Highlight()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (row *CompactRow) UnHighlight() {
|
||||
row.Cols[1].UnHighlight()
|
||||
if config.GetSwitchVal("fullRowCursor") {
|
||||
for _, w := range row.Cols {
|
||||
w.UnHighlight()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type RowBg struct {
|
||||
*ui.Par
|
||||
}
|
||||
|
||||
func NewRowBg() *RowBg {
|
||||
bg := ui.NewPar("")
|
||||
bg.Height = 1
|
||||
bg.Border = false
|
||||
bg.Bg = ui.ThemeAttr("par.text.bg")
|
||||
return &RowBg{bg}
|
||||
}
|
||||
|
||||
func (w *RowBg) Highlight() { w.Bg = ui.ThemeAttr("par.text.fg") }
|
||||
func (w *RowBg) UnHighlight() { w.Bg = ui.ThemeAttr("par.text.bg") }
|
||||
@@ -1,48 +0,0 @@
|
||||
package compact
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/bcicen/ctop/cwidgets"
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
func (row *Compact) SetNet(rx int64, tx int64) {
|
||||
label := fmt.Sprintf("%s / %s", cwidgets.ByteFormat(rx), cwidgets.ByteFormat(tx))
|
||||
row.Net.Set(label)
|
||||
}
|
||||
|
||||
func (row *Compact) SetIO(read int64, write int64) {
|
||||
label := fmt.Sprintf("%s / %s", cwidgets.ByteFormat(read), cwidgets.ByteFormat(write))
|
||||
row.IO.Set(label)
|
||||
}
|
||||
|
||||
func (row *Compact) SetPids(val int) {
|
||||
label := fmt.Sprintf("%s", strconv.Itoa(val))
|
||||
row.Pids.Set(label)
|
||||
}
|
||||
|
||||
func (row *Compact) SetCPU(val int) {
|
||||
row.Cpu.BarColor = colorScale(val)
|
||||
row.Cpu.Label = fmt.Sprintf("%s%%", strconv.Itoa(val))
|
||||
if val < 5 {
|
||||
val = 5
|
||||
row.Cpu.BarColor = ui.ThemeAttr("gauge.bar.bg")
|
||||
}
|
||||
if val > 100 {
|
||||
val = 100
|
||||
}
|
||||
row.Cpu.Percent = val
|
||||
}
|
||||
|
||||
func (row *Compact) SetMem(val int64, limit int64, percent int) {
|
||||
row.Memory.Label = fmt.Sprintf("%s / %s", cwidgets.ByteFormat(val), cwidgets.ByteFormat(limit))
|
||||
if percent < 5 {
|
||||
percent = 5
|
||||
row.Memory.BarColor = ui.ColorBlack
|
||||
} else {
|
||||
row.Memory.BarColor = ui.ThemeAttr("gauge.bar.bg")
|
||||
}
|
||||
row.Memory.Percent = percent
|
||||
}
|
||||
@@ -1,44 +1,96 @@
|
||||
package compact
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/bcicen/ctop/models"
|
||||
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
const (
|
||||
mark = string('\u25C9')
|
||||
vBar = string('\u25AE')
|
||||
statusWidth = 3
|
||||
mark = "◉"
|
||||
healthMark = "✚"
|
||||
vBar = string('\u25AE') + string('\u25AE')
|
||||
)
|
||||
|
||||
// Status indicator
|
||||
type Status struct {
|
||||
*ui.Par
|
||||
*ui.Block
|
||||
status []ui.Cell
|
||||
health []ui.Cell
|
||||
}
|
||||
|
||||
func NewStatus() *Status {
|
||||
p := ui.NewPar(mark)
|
||||
p.Border = false
|
||||
p.Height = 1
|
||||
p.Width = statusWidth
|
||||
return &Status{p}
|
||||
func NewStatus() CompactCol {
|
||||
s := &Status{
|
||||
Block: ui.NewBlock(),
|
||||
health: []ui.Cell{{Ch: ' '}},
|
||||
}
|
||||
s.Height = 1
|
||||
s.Border = false
|
||||
s.setState("")
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Status) Set(val string) {
|
||||
func (s *Status) Buffer() ui.Buffer {
|
||||
buf := s.Block.Buffer()
|
||||
x := 0
|
||||
for _, c := range s.health {
|
||||
buf.Set(s.InnerX()+x, s.InnerY(), c)
|
||||
x += c.Width()
|
||||
}
|
||||
x += 1
|
||||
for _, c := range s.status {
|
||||
buf.Set(s.InnerX()+x, s.InnerY(), c)
|
||||
x += c.Width()
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
func (s *Status) SetMeta(m models.Meta) {
|
||||
s.setState(m.Get("state"))
|
||||
s.setHealth(m.Get("health"))
|
||||
}
|
||||
|
||||
// Status implements CompactCol
|
||||
func (s *Status) Reset() {}
|
||||
func (s *Status) SetMetrics(models.Metrics) {}
|
||||
func (s *Status) Highlight() {}
|
||||
func (s *Status) UnHighlight() {}
|
||||
func (s *Status) Header() string { return "" }
|
||||
func (s *Status) FixedWidth() int { return 3 }
|
||||
|
||||
func (s *Status) setState(val string) {
|
||||
// defaults
|
||||
text := mark
|
||||
color := ui.ColorDefault
|
||||
|
||||
switch val {
|
||||
case "running":
|
||||
color = ui.ColorGreen
|
||||
color = ui.ThemeAttr("status.ok")
|
||||
case "exited":
|
||||
color = ui.ColorRed
|
||||
color = ui.ThemeAttr("status.danger")
|
||||
case "paused":
|
||||
text = fmt.Sprintf("%s%s", vBar, vBar)
|
||||
text = vBar
|
||||
}
|
||||
|
||||
s.Text = text
|
||||
s.TextFgColor = color
|
||||
s.status = ui.TextCells(text, color, ui.ColorDefault)
|
||||
}
|
||||
|
||||
func (s *Status) setHealth(val string) {
|
||||
color := ui.ColorDefault
|
||||
mark := healthMark
|
||||
|
||||
switch val {
|
||||
case "":
|
||||
return
|
||||
case "healthy":
|
||||
color = ui.ThemeAttr("status.ok")
|
||||
case "unhealthy":
|
||||
color = ui.ThemeAttr("status.danger")
|
||||
case "starting":
|
||||
color = ui.ThemeAttr("status.warn")
|
||||
default:
|
||||
log.Warningf("unknown health state string: \"%v\"", val)
|
||||
}
|
||||
|
||||
s.health = ui.TextCells(mark, color, ui.ColorDefault)
|
||||
}
|
||||
|
||||
@@ -1,35 +1,121 @@
|
||||
package compact
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/bcicen/ctop/cwidgets"
|
||||
"github.com/bcicen/ctop/models"
|
||||
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
type TextCol struct {
|
||||
*ui.Par
|
||||
type NameCol struct {
|
||||
*TextCol
|
||||
}
|
||||
|
||||
func NewTextCol(s string) *TextCol {
|
||||
p := ui.NewPar(s)
|
||||
func NewNameCol() CompactCol {
|
||||
return &NameCol{NewTextCol("NAME")}
|
||||
}
|
||||
|
||||
func (w *NameCol) SetMeta(m models.Meta) {
|
||||
w.setText(m.Get("name"))
|
||||
}
|
||||
|
||||
type CIDCol struct {
|
||||
*TextCol
|
||||
}
|
||||
|
||||
func NewCIDCol() CompactCol {
|
||||
c := &CIDCol{NewTextCol("CID")}
|
||||
c.fWidth = 12
|
||||
return c
|
||||
}
|
||||
|
||||
func (w *CIDCol) SetMeta(m models.Meta) {
|
||||
w.setText(m.Get("id"))
|
||||
}
|
||||
|
||||
type NetCol struct {
|
||||
*TextCol
|
||||
}
|
||||
|
||||
func NewNetCol() CompactCol {
|
||||
return &NetCol{NewTextCol("NET RX/TX")}
|
||||
}
|
||||
|
||||
func (w *NetCol) SetMetrics(m models.Metrics) {
|
||||
label := fmt.Sprintf("%s / %s", cwidgets.ByteFormat64Short(m.NetRx), cwidgets.ByteFormat64Short(m.NetTx))
|
||||
w.setText(label)
|
||||
}
|
||||
|
||||
type IOCol struct {
|
||||
*TextCol
|
||||
}
|
||||
|
||||
func NewIOCol() CompactCol {
|
||||
return &IOCol{NewTextCol("IO R/W")}
|
||||
}
|
||||
|
||||
func (w *IOCol) SetMetrics(m models.Metrics) {
|
||||
label := fmt.Sprintf("%s / %s", cwidgets.ByteFormat64Short(m.IOBytesRead), cwidgets.ByteFormat64Short(m.IOBytesWrite))
|
||||
w.setText(label)
|
||||
}
|
||||
|
||||
type PIDCol struct {
|
||||
*TextCol
|
||||
}
|
||||
|
||||
func NewPIDCol() CompactCol {
|
||||
w := &PIDCol{NewTextCol("PIDS")}
|
||||
w.fWidth = 4
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *PIDCol) SetMetrics(m models.Metrics) {
|
||||
w.setText(fmt.Sprintf("%d", m.Pids))
|
||||
}
|
||||
|
||||
type TextCol struct {
|
||||
*ui.Par
|
||||
header string
|
||||
fWidth int
|
||||
}
|
||||
|
||||
func NewTextCol(header string) *TextCol {
|
||||
p := ui.NewPar("-")
|
||||
p.Border = false
|
||||
p.Height = 1
|
||||
p.Width = 20
|
||||
return &TextCol{p}
|
||||
|
||||
return &TextCol{
|
||||
Par: p,
|
||||
header: header,
|
||||
fWidth: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *TextCol) Highlight() {
|
||||
w.Bg = ui.ThemeAttr("par.text.fg")
|
||||
w.TextFgColor = ui.ThemeAttr("par.text.hi")
|
||||
w.TextBgColor = ui.ThemeAttr("par.text.fg")
|
||||
}
|
||||
|
||||
func (w *TextCol) UnHighlight() {
|
||||
w.Bg = ui.ThemeAttr("par.text.bg")
|
||||
w.TextFgColor = ui.ThemeAttr("par.text.fg")
|
||||
w.TextBgColor = ui.ThemeAttr("par.text.bg")
|
||||
}
|
||||
|
||||
func (w *TextCol) Reset() {
|
||||
w.Text = "-"
|
||||
}
|
||||
// TextCol implements CompactCol
|
||||
func (w *TextCol) Reset() { w.setText("-") }
|
||||
func (w *TextCol) SetMeta(models.Meta) {}
|
||||
func (w *TextCol) SetMetrics(models.Metrics) {}
|
||||
func (w *TextCol) Header() string { return w.header }
|
||||
func (w *TextCol) FixedWidth() int { return w.fWidth }
|
||||
|
||||
func (w *TextCol) Set(s string) {
|
||||
func (w *TextCol) setText(s string) {
|
||||
if w.fWidth > 0 && len(s) > w.fWidth {
|
||||
s = s[0:w.fWidth]
|
||||
}
|
||||
w.Text = s
|
||||
}
|
||||
|
||||
@@ -4,36 +4,12 @@ package compact
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
const colSpacing = 1
|
||||
|
||||
// per-column width. 0 == auto width
|
||||
var colWidths = []int{
|
||||
3, // status
|
||||
0, // name
|
||||
0, // cid
|
||||
0, // cpu
|
||||
0, // memory
|
||||
0, // net
|
||||
0, // io
|
||||
4, // pids
|
||||
}
|
||||
|
||||
// Calculate per-column width, given total width
|
||||
func calcWidth(width int) int {
|
||||
spacing := colSpacing * len(colWidths)
|
||||
var staticCols int
|
||||
for _, w := range colWidths {
|
||||
width -= w
|
||||
if w == 0 {
|
||||
staticCols += 1
|
||||
}
|
||||
}
|
||||
return (width - spacing) / staticCols
|
||||
}
|
||||
|
||||
func centerParText(p *ui.Par) {
|
||||
var text string
|
||||
var padding string
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package expanded
|
||||
|
||||
import (
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
var displayInfo = []string{"id", "name", "image", "state"}
|
||||
|
||||
type Info struct {
|
||||
*ui.Table
|
||||
data map[string]string
|
||||
}
|
||||
|
||||
func NewInfo(id string) *Info {
|
||||
p := ui.NewTable()
|
||||
p.Height = 4
|
||||
p.Width = colWidth[0]
|
||||
p.FgColor = ui.ThemeAttr("par.text.fg")
|
||||
p.Separator = false
|
||||
i := &Info{p, make(map[string]string)}
|
||||
i.Set("id", id)
|
||||
return i
|
||||
}
|
||||
|
||||
func (w *Info) Set(k, v string) {
|
||||
w.data[k] = v
|
||||
// rebuild rows
|
||||
w.Rows = [][]string{}
|
||||
for _, k := range displayInfo {
|
||||
if v, ok := w.data[k]; ok {
|
||||
w.Rows = append(w.Rows, []string{k, v})
|
||||
}
|
||||
}
|
||||
w.Height = len(w.Rows) + 2
|
||||
}
|
||||
@@ -2,12 +2,20 @@ package cwidgets
|
||||
|
||||
import (
|
||||
"github.com/bcicen/ctop/logging"
|
||||
"github.com/bcicen/ctop/metrics"
|
||||
"github.com/bcicen/ctop/models"
|
||||
)
|
||||
|
||||
var log = logging.Init()
|
||||
|
||||
type WidgetUpdater interface {
|
||||
SetMeta(string, string)
|
||||
SetMetrics(metrics.Metrics)
|
||||
SetMeta(models.Meta)
|
||||
SetMetrics(models.Metrics)
|
||||
}
|
||||
|
||||
type NullWidgetUpdater struct{}
|
||||
|
||||
// NullWidgetUpdater implements WidgetUpdater
|
||||
func (wu NullWidgetUpdater) SetMeta(models.Meta) {}
|
||||
|
||||
// NullWidgetUpdater implements WidgetUpdater
|
||||
func (wu NullWidgetUpdater) SetMetrics(models.Metrics) {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package expanded
|
||||
package single
|
||||
|
||||
import (
|
||||
ui "github.com/gizak/termui"
|
||||
36
cwidgets/single/env.go
Normal file
36
cwidgets/single/env.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package single
|
||||
|
||||
import (
|
||||
ui "github.com/gizak/termui"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
var envPattern = regexp.MustCompile(`(?P<KEY>[^=]+)=(?P<VALUJE>.*)`)
|
||||
|
||||
type Env struct {
|
||||
*ui.Table
|
||||
data map[string]string
|
||||
}
|
||||
|
||||
func NewEnv() *Env {
|
||||
p := ui.NewTable()
|
||||
p.Height = 4
|
||||
p.Width = colWidth[0]
|
||||
p.FgColor = ui.ThemeAttr("par.text.fg")
|
||||
p.Separator = false
|
||||
i := &Env{p, make(map[string]string)}
|
||||
i.BorderLabel = "Env"
|
||||
return i
|
||||
}
|
||||
|
||||
func (w *Env) Set(k, v string) {
|
||||
match := envPattern.FindStringSubmatch(v)
|
||||
key := match[1]
|
||||
value := match[2]
|
||||
w.data[key] = value
|
||||
|
||||
w.Rows = [][]string{}
|
||||
w.Rows = append(w.Rows, mkInfoRows(key, value)...)
|
||||
|
||||
w.Height = len(w.Rows) + 2
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package expanded
|
||||
package single
|
||||
|
||||
type IntHist struct {
|
||||
Val int // most current data point
|
||||
58
cwidgets/single/info.go
Normal file
58
cwidgets/single/info.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package single
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
var displayInfo = []string{"id", "name", "image", "ports", "IPs", "state", "created", "health"}
|
||||
|
||||
type Info struct {
|
||||
*ui.Table
|
||||
data map[string]string
|
||||
}
|
||||
|
||||
func NewInfo(id string) *Info {
|
||||
p := ui.NewTable()
|
||||
p.Height = 4
|
||||
p.Width = colWidth[0]
|
||||
p.FgColor = ui.ThemeAttr("par.text.fg")
|
||||
p.Separator = false
|
||||
i := &Info{p, make(map[string]string)}
|
||||
i.Set("id", id)
|
||||
return i
|
||||
}
|
||||
|
||||
func (w *Info) Set(k, v string) {
|
||||
w.data[k] = v
|
||||
|
||||
// rebuild rows
|
||||
w.Rows = [][]string{}
|
||||
for _, k := range displayInfo {
|
||||
if v, ok := w.data[k]; ok {
|
||||
w.Rows = append(w.Rows, mkInfoRows(k, v)...)
|
||||
}
|
||||
}
|
||||
|
||||
w.Height = len(w.Rows) + 2
|
||||
}
|
||||
|
||||
// Build row(s) from a key and value string
|
||||
func mkInfoRows(k, v string) (rows [][]string) {
|
||||
lines := strings.Split(v, "\n")
|
||||
|
||||
// initial row with field name
|
||||
rows = append(rows, []string{k, lines[0]})
|
||||
|
||||
// append any additional lines in separate row
|
||||
if len(lines) > 1 {
|
||||
for _, line := range lines[1:] {
|
||||
if line != "" {
|
||||
rows = append(rows, []string{"", line})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rows
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package expanded
|
||||
package single
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -42,10 +42,10 @@ func (w *IO) Update(read int64, write int64) {
|
||||
var rate string
|
||||
|
||||
w.readHist.Append(int(read))
|
||||
rate = strings.ToLower(cwidgets.ByteFormatInt(w.readHist.Val))
|
||||
rate = strings.ToLower(cwidgets.ByteFormatShort(w.readHist.Val))
|
||||
w.Lines[0].Title = fmt.Sprintf("read [%s/s]", rate)
|
||||
|
||||
w.writeHist.Append(int(write))
|
||||
rate = strings.ToLower(cwidgets.ByteFormatInt(w.writeHist.Val))
|
||||
rate = strings.ToLower(cwidgets.ByteFormatShort(w.writeHist.Val))
|
||||
w.Lines[1].Title = fmt.Sprintf("write [%s/s]", rate)
|
||||
}
|
||||
83
cwidgets/single/logs.go
Normal file
83
cwidgets/single/logs.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package single
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/bcicen/ctop/models"
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
type LogLines struct {
|
||||
ts []time.Time
|
||||
data []string
|
||||
}
|
||||
|
||||
func NewLogLines(max int) *LogLines {
|
||||
ll := &LogLines{
|
||||
ts: make([]time.Time, max),
|
||||
data: make([]string, max),
|
||||
}
|
||||
return ll
|
||||
}
|
||||
|
||||
func (ll *LogLines) tail(n int) []string {
|
||||
lines := make([]string, n)
|
||||
for i := 0; i < n; i++ {
|
||||
lines = append(lines, ll.data[len(ll.data)-i])
|
||||
}
|
||||
return lines
|
||||
}
|
||||
func (ll *LogLines) getLines(start, end int) []string {
|
||||
if end < 0 {
|
||||
return ll.data[start:]
|
||||
}
|
||||
return ll.data[start:end]
|
||||
}
|
||||
|
||||
func (ll *LogLines) add(l models.Log) {
|
||||
if len(ll.data) == cap(ll.data) {
|
||||
ll.data = append(ll.data[:0], ll.data[1:]...)
|
||||
ll.ts = append(ll.ts[:0], ll.ts[1:]...)
|
||||
}
|
||||
ll.ts = append(ll.ts, l.Timestamp)
|
||||
ll.data = append(ll.data, l.Message)
|
||||
log.Debugf("recorded log line: %v", l)
|
||||
}
|
||||
|
||||
type Logs struct {
|
||||
*ui.List
|
||||
lines *LogLines
|
||||
}
|
||||
|
||||
func NewLogs(stream chan models.Log) *Logs {
|
||||
p := ui.NewList()
|
||||
p.Y = ui.TermHeight() / 2
|
||||
p.X = 0
|
||||
p.Height = ui.TermHeight() - p.Y
|
||||
p.Width = ui.TermWidth()
|
||||
//p.Overflow = "wrap"
|
||||
p.ItemFgColor = ui.ThemeAttr("par.text.fg")
|
||||
i := &Logs{p, NewLogLines(4098)}
|
||||
go func() {
|
||||
for line := range stream {
|
||||
i.lines.add(line)
|
||||
ui.Render(i)
|
||||
}
|
||||
}()
|
||||
return i
|
||||
}
|
||||
|
||||
func (w *Logs) Align() {
|
||||
w.X = colWidth[0]
|
||||
w.List.Align()
|
||||
}
|
||||
|
||||
func (w *Logs) Buffer() ui.Buffer {
|
||||
maxLines := w.Height - 2
|
||||
offset := len(w.lines.data) - maxLines
|
||||
w.Items = w.lines.getLines(offset, -1)
|
||||
return w.List.Buffer()
|
||||
}
|
||||
|
||||
// number of rows a line will occupy at current panel width
|
||||
func (w *Logs) lineHeight(s string) int { return (len(s) / w.InnerWidth()) + 1 }
|
||||
@@ -1,8 +1,8 @@
|
||||
package expanded
|
||||
package single
|
||||
|
||||
import (
|
||||
"github.com/bcicen/ctop/logging"
|
||||
"github.com/bcicen/ctop/metrics"
|
||||
"github.com/bcicen/ctop/models"
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
@@ -12,31 +12,33 @@ var (
|
||||
colWidth = [2]int{65, 0} // left,right column width
|
||||
)
|
||||
|
||||
type Expanded struct {
|
||||
type Single struct {
|
||||
Info *Info
|
||||
Net *Net
|
||||
Cpu *Cpu
|
||||
Mem *Mem
|
||||
IO *IO
|
||||
Env *Env
|
||||
X, Y int
|
||||
Width int
|
||||
}
|
||||
|
||||
func NewExpanded(id string) *Expanded {
|
||||
func NewSingle(id string) *Single {
|
||||
if len(id) > 12 {
|
||||
id = id[:12]
|
||||
}
|
||||
return &Expanded{
|
||||
return &Single{
|
||||
Info: NewInfo(id),
|
||||
Net: NewNet(),
|
||||
Cpu: NewCpu(),
|
||||
Mem: NewMem(),
|
||||
IO: NewIO(),
|
||||
Env: NewEnv(),
|
||||
Width: ui.TermWidth(),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Expanded) Up() {
|
||||
func (e *Single) Up() {
|
||||
if e.Y < 0 {
|
||||
e.Y++
|
||||
e.Align()
|
||||
@@ -44,7 +46,7 @@ func (e *Expanded) Up() {
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Expanded) Down() {
|
||||
func (e *Single) Down() {
|
||||
if e.Y > (ui.TermHeight() - e.GetHeight()) {
|
||||
e.Y--
|
||||
e.Align()
|
||||
@@ -52,27 +54,36 @@ func (e *Expanded) Down() {
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Expanded) SetWidth(w int) { e.Width = w }
|
||||
func (e *Expanded) SetMeta(k, v string) { e.Info.Set(k, v) }
|
||||
func (e *Single) SetWidth(w int) { e.Width = w }
|
||||
func (e *Single) SetMeta(m models.Meta) {
|
||||
for k, v := range m {
|
||||
if k == "[ENV-VAR]" {
|
||||
e.Env.Set(k, v)
|
||||
} else {
|
||||
e.Info.Set(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Expanded) SetMetrics(m metrics.Metrics) {
|
||||
func (e *Single) SetMetrics(m models.Metrics) {
|
||||
e.Cpu.Update(m.CPUUtil)
|
||||
e.Net.Update(m.NetRx, m.NetTx)
|
||||
e.Mem.Update(int(m.MemUsage), int(m.MemLimit))
|
||||
e.IO.Update(m.IOBytesRead, m.IOBytesWrite)
|
||||
}
|
||||
|
||||
// Return total column height
|
||||
func (e *Expanded) GetHeight() (h int) {
|
||||
// GetHeight returns total column height
|
||||
func (e *Single) GetHeight() (h int) {
|
||||
h += e.Info.Height
|
||||
h += e.Net.Height
|
||||
h += e.Cpu.Height
|
||||
h += e.Mem.Height
|
||||
h += e.IO.Height
|
||||
h += e.Env.Height
|
||||
return h
|
||||
}
|
||||
|
||||
func (e *Expanded) Align() {
|
||||
func (e *Single) Align() {
|
||||
// reset offset if needed
|
||||
if e.GetHeight() <= ui.TermHeight() {
|
||||
e.Y = 0
|
||||
@@ -91,10 +102,7 @@ func (e *Expanded) Align() {
|
||||
log.Debugf("align: width=%v left-col=%v right-col=%v", e.Width, colWidth[0], colWidth[1])
|
||||
}
|
||||
|
||||
func calcWidth(w int) {
|
||||
}
|
||||
|
||||
func (e *Expanded) Buffer() ui.Buffer {
|
||||
func (e *Single) Buffer() ui.Buffer {
|
||||
buf := ui.NewBuffer()
|
||||
if e.Width < (colWidth[0] + colWidth[1]) {
|
||||
ui.Clear()
|
||||
@@ -106,16 +114,18 @@ func (e *Expanded) Buffer() ui.Buffer {
|
||||
buf.Merge(e.Mem.Buffer())
|
||||
buf.Merge(e.Net.Buffer())
|
||||
buf.Merge(e.IO.Buffer())
|
||||
buf.Merge(e.Env.Buffer())
|
||||
return buf
|
||||
}
|
||||
|
||||
func (e *Expanded) all() []ui.GridBufferer {
|
||||
func (e *Single) all() []ui.GridBufferer {
|
||||
return []ui.GridBufferer{
|
||||
e.Info,
|
||||
e.Cpu,
|
||||
e.Mem,
|
||||
e.Net,
|
||||
e.IO,
|
||||
e.Env,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package expanded
|
||||
package single
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -70,7 +70,7 @@ func newMemChart() *ui.MBarChart {
|
||||
mbar.BarColor[1] = ui.ColorBlack
|
||||
mbar.NumColor[1] = ui.ColorBlack
|
||||
|
||||
mbar.NumFmt = cwidgets.ByteFormatInt
|
||||
mbar.NumFmt = cwidgets.ByteFormatShort
|
||||
//mbar.ShowScale = true
|
||||
return mbar
|
||||
}
|
||||
@@ -78,6 +78,6 @@ func newMemChart() *ui.MBarChart {
|
||||
func (w *Mem) Update(val int, limit int) {
|
||||
w.valHist.Append(val)
|
||||
w.limitHist.Append(limit - val)
|
||||
w.InnerLabel.Text = fmt.Sprintf("%v / %v", cwidgets.ByteFormatInt(val), cwidgets.ByteFormatInt(limit))
|
||||
w.InnerLabel.Text = fmt.Sprintf("%v / %v", cwidgets.ByteFormatShort(val), cwidgets.ByteFormatShort(limit))
|
||||
//w.Data[0] = w.hist.data
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package expanded
|
||||
package single
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -42,10 +42,10 @@ func (w *Net) Update(rx int64, tx int64) {
|
||||
var rate string
|
||||
|
||||
w.rxHist.Append(int(rx))
|
||||
rate = strings.ToLower(cwidgets.ByteFormatInt(w.rxHist.Val))
|
||||
rate = strings.ToLower(cwidgets.ByteFormat(w.rxHist.Val))
|
||||
w.Lines[0].Title = fmt.Sprintf("RX [%s/s]", rate)
|
||||
|
||||
w.txHist.Append(int(tx))
|
||||
rate = strings.ToLower(cwidgets.ByteFormatInt(w.txHist.Val))
|
||||
rate = strings.ToLower(cwidgets.ByteFormat(w.txHist.Val))
|
||||
w.Lines[1].Title = fmt.Sprintf("TX [%s/s]", rate)
|
||||
}
|
||||
@@ -1,48 +1,74 @@
|
||||
package cwidgets
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
kb = 1024
|
||||
mb = kb * 1024
|
||||
gb = mb * 1024
|
||||
// byte ratio constants
|
||||
_ = iota
|
||||
kib float64 = 1 << (10 * iota)
|
||||
mib
|
||||
gib
|
||||
tib
|
||||
pib
|
||||
)
|
||||
|
||||
// convenience method
|
||||
func ByteFormatInt(n int) string {
|
||||
return ByteFormat(int64(n))
|
||||
var (
|
||||
units = []float64{
|
||||
1,
|
||||
kib,
|
||||
mib,
|
||||
gib,
|
||||
tib,
|
||||
pib,
|
||||
}
|
||||
|
||||
// short, full unit labels
|
||||
labels = [][2]string{
|
||||
[2]string{"B", "B"},
|
||||
[2]string{"K", "KiB"},
|
||||
[2]string{"M", "MiB"},
|
||||
[2]string{"G", "GiB"},
|
||||
[2]string{"T", "TiB"},
|
||||
[2]string{"P", "PiB"},
|
||||
}
|
||||
)
|
||||
|
||||
// convenience methods
|
||||
func ByteFormat(n int) string { return byteFormat(float64(n), false) }
|
||||
func ByteFormatShort(n int) string { return byteFormat(float64(n), true) }
|
||||
func ByteFormat64(n int64) string { return byteFormat(float64(n), false) }
|
||||
func ByteFormat64Short(n int64) string { return byteFormat(float64(n), true) }
|
||||
|
||||
func byteFormat(n float64, short bool) string {
|
||||
i := len(units) - 1
|
||||
|
||||
for i > 0 {
|
||||
if n >= units[i] {
|
||||
n /= units[i]
|
||||
break
|
||||
}
|
||||
i--
|
||||
}
|
||||
|
||||
if short {
|
||||
return unpadFloat(n, 0) + labels[i][0]
|
||||
}
|
||||
return unpadFloat(n, 2) + labels[i][1]
|
||||
}
|
||||
|
||||
func ByteFormat(n int64) string {
|
||||
if n < kb {
|
||||
return fmt.Sprintf("%sB", strconv.FormatInt(n, 10))
|
||||
}
|
||||
if n < mb {
|
||||
n = n / kb
|
||||
return fmt.Sprintf("%sK", strconv.FormatInt(n, 10))
|
||||
}
|
||||
if n < gb {
|
||||
n = n / mb
|
||||
return fmt.Sprintf("%sM", strconv.FormatInt(n, 10))
|
||||
}
|
||||
nf := float64(n) / gb
|
||||
return fmt.Sprintf("%sG", unpadFloat(nf))
|
||||
func unpadFloat(f float64, maxp int) string {
|
||||
return strconv.FormatFloat(f, 'f', getPrecision(f, maxp), 64)
|
||||
}
|
||||
|
||||
func unpadFloat(f float64) string {
|
||||
return strconv.FormatFloat(f, 'f', getPrecision(f), 64)
|
||||
}
|
||||
|
||||
func getPrecision(f float64) int {
|
||||
func getPrecision(f float64, maxp int) int {
|
||||
frac := int((f - float64(int(f))) * 100)
|
||||
if frac == 0 {
|
||||
if frac == 0 || maxp == 0 {
|
||||
return 0
|
||||
}
|
||||
if frac%10 == 0 {
|
||||
if frac%10 == 0 || maxp < 2 {
|
||||
return 1
|
||||
}
|
||||
return 2 // default precision
|
||||
return maxp
|
||||
}
|
||||
|
||||
22
debug.go
22
debug.go
@@ -3,10 +3,14 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime"
|
||||
|
||||
"github.com/bcicen/ctop/container"
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
var mstats = &runtime.MemStats{}
|
||||
|
||||
func logEvent(e ui.Event) {
|
||||
var s string
|
||||
s += fmt.Sprintf("Type=%s", quote(e.Type))
|
||||
@@ -18,8 +22,24 @@ func logEvent(e ui.Event) {
|
||||
log.Debugf("new event: %s", s)
|
||||
}
|
||||
|
||||
func runtimeStats() {
|
||||
var msg string
|
||||
msg += fmt.Sprintf("cgo calls=%v", runtime.NumCgoCall())
|
||||
msg += fmt.Sprintf(" routines=%v", runtime.NumGoroutine())
|
||||
runtime.ReadMemStats(mstats)
|
||||
msg += fmt.Sprintf(" numgc=%v", mstats.NumGC)
|
||||
msg += fmt.Sprintf(" alloc=%v", mstats.Alloc)
|
||||
log.Debugf("runtime: %v", msg)
|
||||
}
|
||||
|
||||
func runtimeStack() {
|
||||
buf := make([]byte, 32768)
|
||||
buf = buf[:runtime.Stack(buf, true)]
|
||||
log.Infof(fmt.Sprintf("stack:\n%v", string(buf)))
|
||||
}
|
||||
|
||||
// log container, metrics, and widget state
|
||||
func dumpContainer(c *Container) {
|
||||
func dumpContainer(c *container.Container) {
|
||||
msg := fmt.Sprintf("logging state for container: %s\n", c.Id)
|
||||
for k, v := range c.Meta {
|
||||
msg += fmt.Sprintf("Meta.%s = %s\n", k, v)
|
||||
|
||||
156
dockersource.go
156
dockersource.go
@@ -1,156 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/bcicen/ctop/metrics"
|
||||
"github.com/fsouza/go-dockerclient"
|
||||
)
|
||||
|
||||
type ContainerSource interface {
|
||||
All() Containers
|
||||
Get(string) (*Container, bool)
|
||||
}
|
||||
|
||||
type DockerContainerSource struct {
|
||||
client *docker.Client
|
||||
containers map[string]*Container
|
||||
needsRefresh chan string // container IDs requiring refresh
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func NewDockerContainerSource() *DockerContainerSource {
|
||||
// init docker client
|
||||
client, err := docker.NewClientFromEnv()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cm := &DockerContainerSource{
|
||||
client: client,
|
||||
containers: make(map[string]*Container),
|
||||
needsRefresh: make(chan string, 60),
|
||||
lock: sync.RWMutex{},
|
||||
}
|
||||
go cm.Loop()
|
||||
cm.refreshAll()
|
||||
go cm.watchEvents()
|
||||
return cm
|
||||
}
|
||||
|
||||
// Docker events watcher
|
||||
func (cm *DockerContainerSource) watchEvents() {
|
||||
log.Info("docker event listener starting")
|
||||
events := make(chan *docker.APIEvents)
|
||||
cm.client.AddEventListener(events)
|
||||
|
||||
for e := range events {
|
||||
if e.Type != "container" {
|
||||
continue
|
||||
}
|
||||
switch e.Action {
|
||||
case "start", "die", "pause", "unpause":
|
||||
log.Debugf("handling docker event: action=%s id=%s", e.Action, e.ID)
|
||||
cm.needsRefresh <- e.ID
|
||||
case "destroy":
|
||||
log.Debugf("handling docker event: action=%s id=%s", e.Action, e.ID)
|
||||
cm.delByID(e.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (cm *DockerContainerSource) refresh(c *Container) {
|
||||
insp := cm.inspect(c.Id)
|
||||
// remove container if no longer exists
|
||||
if insp == nil {
|
||||
cm.delByID(c.Id)
|
||||
return
|
||||
}
|
||||
c.SetMeta("name", shortName(insp.Name))
|
||||
c.SetMeta("image", insp.Config.Image)
|
||||
c.SetMeta("created", insp.Created.Format("Mon Jan 2 15:04:05 2006"))
|
||||
c.SetState(insp.State.Status)
|
||||
}
|
||||
|
||||
func (cm *DockerContainerSource) inspect(id string) *docker.Container {
|
||||
c, err := cm.client.InspectContainer(id)
|
||||
if err != nil {
|
||||
if _, ok := err.(*docker.NoSuchContainer); ok == false {
|
||||
log.Errorf(err.Error())
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Mark all container IDs for refresh
|
||||
func (cm *DockerContainerSource) refreshAll() {
|
||||
opts := docker.ListContainersOptions{All: true}
|
||||
allContainers, err := cm.client.ListContainers(opts)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, i := range allContainers {
|
||||
c := cm.MustGet(i.ID)
|
||||
c.SetMeta("name", shortName(i.Names[0]))
|
||||
c.SetState(i.State)
|
||||
cm.needsRefresh <- c.Id
|
||||
}
|
||||
}
|
||||
|
||||
func (cm *DockerContainerSource) Loop() {
|
||||
for id := range cm.needsRefresh {
|
||||
c := cm.MustGet(id)
|
||||
cm.refresh(c)
|
||||
}
|
||||
}
|
||||
|
||||
// Get a single container, creating one anew if not existing
|
||||
func (cm *DockerContainerSource) MustGet(id string) *Container {
|
||||
c, ok := cm.Get(id)
|
||||
// append container struct for new containers
|
||||
if !ok {
|
||||
// create collector
|
||||
collector := metrics.NewDocker(cm.client, id)
|
||||
// create container
|
||||
c = NewContainer(id, collector)
|
||||
cm.lock.Lock()
|
||||
cm.containers[id] = c
|
||||
cm.lock.Unlock()
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// Get a single container, by ID
|
||||
func (cm *DockerContainerSource) Get(id string) (*Container, bool) {
|
||||
cm.lock.Lock()
|
||||
c, ok := cm.containers[id]
|
||||
cm.lock.Unlock()
|
||||
return c, ok
|
||||
}
|
||||
|
||||
// Remove containers by ID
|
||||
func (cm *DockerContainerSource) delByID(id string) {
|
||||
cm.lock.Lock()
|
||||
delete(cm.containers, id)
|
||||
cm.lock.Unlock()
|
||||
log.Infof("removed dead container: %s", id)
|
||||
}
|
||||
|
||||
// Return array of all containers, sorted by field
|
||||
func (cm *DockerContainerSource) All() (containers Containers) {
|
||||
cm.lock.Lock()
|
||||
for _, c := range cm.containers {
|
||||
containers = append(containers, c)
|
||||
}
|
||||
cm.lock.Unlock()
|
||||
sort.Sort(containers)
|
||||
containers.Filter()
|
||||
return containers
|
||||
}
|
||||
|
||||
// use primary container name
|
||||
func shortName(name string) string {
|
||||
return strings.Replace(name, "/", "", 1)
|
||||
}
|
||||
85
glide.lock
generated
85
glide.lock
generated
@@ -1,85 +0,0 @@
|
||||
hash: c13011881e895378f374b68596a59a0ea6def372b4f5239b2d8aa342eaa46a4b
|
||||
updated: 2017-03-12T09:53:35.212073637+07:00
|
||||
imports:
|
||||
- name: github.com/Azure/go-ansiterm
|
||||
version: fa152c58bc15761d0200cb75fe958b89a9d4888e
|
||||
subpackages:
|
||||
- winterm
|
||||
- name: github.com/docker/docker
|
||||
version: ce07fb6b0f1b8765b92022e45f96bd4349812e06
|
||||
subpackages:
|
||||
- api/types
|
||||
- api/types/blkiodev
|
||||
- api/types/container
|
||||
- api/types/filters
|
||||
- api/types/mount
|
||||
- api/types/network
|
||||
- api/types/registry
|
||||
- api/types/strslice
|
||||
- api/types/swarm
|
||||
- api/types/versions
|
||||
- opts
|
||||
- pkg/archive
|
||||
- pkg/fileutils
|
||||
- pkg/homedir
|
||||
- pkg/idtools
|
||||
- pkg/ioutils
|
||||
- pkg/jsonlog
|
||||
- pkg/jsonmessage
|
||||
- pkg/longpath
|
||||
- pkg/pools
|
||||
- pkg/promise
|
||||
- pkg/stdcopy
|
||||
- pkg/system
|
||||
- pkg/term
|
||||
- pkg/term/windows
|
||||
- name: github.com/docker/go-connections
|
||||
version: a2afab9802043837035592f1c24827fb70766de9
|
||||
subpackages:
|
||||
- nat
|
||||
- name: github.com/docker/go-units
|
||||
version: 0dadbb0345b35ec7ef35e228dabb8de89a65bf52
|
||||
- name: github.com/fsouza/go-dockerclient
|
||||
version: 318513eb1ab27495afbc67f671ba1080513d8aa0
|
||||
- name: github.com/gizak/termui
|
||||
version: ea10e6ccee219e572ffad0ac1909f1a17f6db7d6
|
||||
repo: https://github.com/bcicen/termui
|
||||
vcs: git
|
||||
- name: github.com/hashicorp/go-cleanhttp
|
||||
version: 3573b8b52aa7b37b9358d966a898feb387f62437
|
||||
- name: github.com/jgautheron/codename-generator
|
||||
version: 16d037c7cc3c9b552fe4af9828b7338d752dbaf9
|
||||
- name: github.com/maruel/panicparse
|
||||
version: 25bcac0d793cf4109483505a0d66e066a3a90a80
|
||||
subpackages:
|
||||
- stack
|
||||
- name: github.com/mattn/go-runewidth
|
||||
version: 14207d285c6c197daabb5c9793d63e7af9ab2d50
|
||||
- name: github.com/Microsoft/go-winio
|
||||
version: fff283ad5116362ca252298cfc9b95828956d85d
|
||||
- name: github.com/mitchellh/go-wordwrap
|
||||
version: ad45545899c7b13c020ea92b2072220eefad42b8
|
||||
- name: github.com/nsf/termbox-go
|
||||
version: 91bae1bb5fa9ee504905ecbe7043fa30e92feaa3
|
||||
- name: github.com/nu7hatch/gouuid
|
||||
version: 179d4d0c4d8d407a32af483c2354df1d2c91e6c3
|
||||
- name: github.com/op/go-logging
|
||||
version: b2cb9fa56473e98db8caba80237377e83fe44db5
|
||||
- name: github.com/opencontainers/runc
|
||||
version: 31980a53ae7887b2c8f8715d13c3eb486c27b6cf
|
||||
subpackages:
|
||||
- libcontainer/system
|
||||
- libcontainer/user
|
||||
- name: github.com/Sirupsen/logrus
|
||||
version: 1deb2db2a6fff8a35532079061b903c3a25eed52
|
||||
- name: golang.org/x/net
|
||||
version: a6577fac2d73be281a500b310739095313165611
|
||||
subpackages:
|
||||
- context
|
||||
- context/ctxhttp
|
||||
- name: golang.org/x/sys
|
||||
version: 99f16d856c9836c42d24e7ab64ea72916925fa97
|
||||
subpackages:
|
||||
- unix
|
||||
- windows
|
||||
testImports: []
|
||||
11
glide.yaml
11
glide.yaml
@@ -1,11 +0,0 @@
|
||||
package: github.com/bcicen/ctop
|
||||
import:
|
||||
- package: github.com/fsouza/go-dockerclient
|
||||
- package: github.com/gizak/termui
|
||||
version: barchart-numfmt
|
||||
repo: https://github.com/bcicen/termui
|
||||
vcs: git
|
||||
- package: github.com/jgautheron/codename-generator
|
||||
- package: github.com/nu7hatch/gouuid
|
||||
- package: github.com/op/go-logging
|
||||
version: ^1.0.0
|
||||
21
go.mod
Normal file
21
go.mod
Normal file
@@ -0,0 +1,21 @@
|
||||
module github.com/bcicen/ctop
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v0.3.1
|
||||
github.com/c9s/goprocinfo v0.0.0-20170609001544-b34328d6e0cd
|
||||
github.com/fsouza/go-dockerclient v1.6.6
|
||||
github.com/gizak/termui v2.3.0+incompatible
|
||||
github.com/jgautheron/codename-generator v0.0.0-20150829203204-16d037c7cc3c
|
||||
github.com/mattn/go-runewidth v0.0.0-20170201023540-14207d285c6c // indirect
|
||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
|
||||
github.com/nsf/termbox-go v0.0.0-20180303152453-e2050e41c884
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d
|
||||
github.com/op/go-logging v0.0.0-20160211212156-b2cb9fa56473
|
||||
github.com/opencontainers/runc v1.0.0-rc92
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/stretchr/testify v1.4.0
|
||||
)
|
||||
|
||||
replace github.com/gizak/termui => github.com/bcicen/termui v0.0.0-20180326052246-4eb80249d3f5
|
||||
|
||||
go 1.15
|
||||
280
go.sum
Normal file
280
go.sum
Normal file
@@ -0,0 +1,280 @@
|
||||
bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
|
||||
github.com/Microsoft/go-winio v0.4.15-0.20200113171025-3fe6c5262873 h1:93nQ7k53GjoMQ07HVP8g6Zj1fQZDDj7Xy2VkNNtvX8o=
|
||||
github.com/Microsoft/go-winio v0.4.15-0.20200113171025-3fe6c5262873/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
|
||||
github.com/Microsoft/hcsshim v0.8.9 h1:VrfodqvztU8YSOvygU+DN1BGaSGxmrNfqOv5oOuX2Bk=
|
||||
github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8=
|
||||
github.com/bcicen/termui v0.0.0-20180326052246-4eb80249d3f5 h1:2pI3ZsoefWIi++8EqmANoC7Px/v2lRwnleVUcCuFgLg=
|
||||
github.com/bcicen/termui v0.0.0-20180326052246-4eb80249d3f5/go.mod h1:yIA9ITWZD1p4/DvCQ44xvhyVb9XEUlVnY1rzGSHwbiM=
|
||||
github.com/c9s/goprocinfo v0.0.0-20170609001544-b34328d6e0cd h1:xqaBnULC8wEnQpRDXAsDgXkU/STqoluz1REOoegSfNU=
|
||||
github.com/c9s/goprocinfo v0.0.0-20170609001544-b34328d6e0cd/go.mod h1:uEyr4WpAH4hio6LFriaPkL938XnrvLpNPmQHBdrmbIE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/checkpoint-restore/go-criu/v4 v4.1.0 h1:WW2B2uxx9KWF6bGlHqhm8Okiafwwx7Y2kcpn8lCpjgo=
|
||||
github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
|
||||
github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775 h1:cHzBGGVew0ezFsq2grfy2RsB8hO/eNyBgOLHBCqfR1U=
|
||||
github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko=
|
||||
github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
|
||||
github.com/containerd/console v1.0.0 h1:fU3UuQapBs+zLJu82NhR11Rif1ny2zfMMAyPJzSN5tQ=
|
||||
github.com/containerd/console v1.0.0/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE=
|
||||
github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
||||
github.com/containerd/containerd v1.3.4 h1:3o0smo5SKY7H6AJCmJhsnCjR2/V2T8VmiHt7seN2/kI=
|
||||
github.com/containerd/containerd v1.3.4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
||||
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
|
||||
github.com/containerd/continuity v0.0.0-20200413184840-d3ef23f19fbb h1:nXPkFq8X1a9ycY3GYQpFNxHh3j2JgY7zDZfq2EXMIzk=
|
||||
github.com/containerd/continuity v0.0.0-20200413184840-d3ef23f19fbb/go.mod h1:Dq467ZllaHgAtVp4p1xUQWBrFXR9s/wyoTpG8zOJGkY=
|
||||
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
|
||||
github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=
|
||||
github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
|
||||
github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd/v22 v22.1.0 h1:kq/SbG2BCKLkDKkjQf5OWwKWUKj1lgs3lFI4PxnR5lg=
|
||||
github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/cyphar/filepath-securejoin v0.2.2 h1:jCwT2GTP+PY5nBz3c/YL5PAIbusElVrPujOBSCj8xRg=
|
||||
github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
|
||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v17.12.0-ce-rc1.0.20200505174321-1655290016ac+incompatible h1:ZxJX4ZSNg1LORBsStUojbrLfkrE3Ut122XhzyZnN110=
|
||||
github.com/docker/docker v17.12.0-ce-rc1.0.20200505174321-1655290016ac+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsouza/go-dockerclient v1.6.6 h1:9e3xkBrVkPb81gzYq23i7iDUEd6sx2ooeJA/gnYU6R4=
|
||||
github.com/fsouza/go-dockerclient v1.6.6/go.mod h1:3/oRIWoe7uT6bwtAayj/EmJmepBjeL4pYvt7ZxC7Rnk=
|
||||
github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e h1:BWhy2j3IXJhjCbC68FptL43tDKIq8FladmaTs3Xs7Z8=
|
||||
github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
|
||||
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
|
||||
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jgautheron/codename-generator v0.0.0-20150829203204-16d037c7cc3c h1:/hc+TxW4Q1v6aqNPHE5jiaNF2xEK0CzWTgo25RQhQ+U=
|
||||
github.com/jgautheron/codename-generator v0.0.0-20150829203204-16d037c7cc3c/go.mod h1:FJRkXmPrkHw0WDjB/LXMUhjWJ112Y6JUYnIVBOy8oH8=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/mattn/go-runewidth v0.0.0-20170201023540-14207d285c6c h1:eFzthqtg3W6Pihj3DMTXLAF4f+ge5r5Ie5g6HLIZAF0=
|
||||
github.com/mattn/go-runewidth v0.0.0-20170201023540-14207d285c6c/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
|
||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
||||
github.com/moby/sys/mount v0.1.0 h1:Ytx78EatgFKtrqZ0BvJ0UtJE472ZvawVmil6pIfuCCU=
|
||||
github.com/moby/sys/mount v0.1.0/go.mod h1:FVQFLDRWwyBjDTBNQXDlWnSFREqOo3OKX9aqhmeoo74=
|
||||
github.com/moby/sys/mountinfo v0.1.0/go.mod h1:w2t2Avltqx8vE7gX5l+QiBKxODu2TX0+Syr3h52Tw4o=
|
||||
github.com/moby/sys/mountinfo v0.1.3 h1:KIrhRO14+AkwKvG/g2yIpNMOUVZ02xNhOw8KY1WsLOI=
|
||||
github.com/moby/sys/mountinfo v0.1.3/go.mod h1:w2t2Avltqx8vE7gX5l+QiBKxODu2TX0+Syr3h52Tw4o=
|
||||
github.com/moby/term v0.0.0-20200429084858-129dac9f73f6 h1:3Y9aosU6S5Bo8GYH0s+t1ej4m30GuUKvQ3c9ZLqdL28=
|
||||
github.com/moby/term v0.0.0-20200429084858-129dac9f73f6/go.mod h1:or9wGItza1sRcM4Wd3dIv8DsFHYQuFsMHEdxUIlUxms=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/mrunalp/fileutils v0.0.0-20200520151820-abd8a0e76976 h1:aZQToFSLH8ejFeSkTc3r3L4dPImcj7Ib/KgmkQqbGGg=
|
||||
github.com/mrunalp/fileutils v0.0.0-20200520151820-abd8a0e76976/go.mod h1:x8F1gnqOkIEiO4rqoeEEEqQbo7HjGMTvyoq3gej4iT0=
|
||||
github.com/nsf/termbox-go v0.0.0-20180303152453-e2050e41c884 h1:fcs71SMqqDhUD+PbpIv9xf3EH9F9s6HfiLwr6jKm1VA=
|
||||
github.com/nsf/termbox-go v0.0.0-20180303152453-e2050e41c884/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ=
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
|
||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/op/go-logging v0.0.0-20160211212156-b2cb9fa56473 h1:J1QZwDXgZ4dJD2s19iqR9+U00OWM2kDzbf1O/fmvCWg=
|
||||
github.com/op/go-logging v0.0.0-20160211212156-b2cb9fa56473/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
|
||||
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
|
||||
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
|
||||
github.com/opencontainers/runc v1.0.0-rc92 h1:+IczUKCRzDzFDnw99O/PAqrcBBCoRp9xN3cB1SYSNS4=
|
||||
github.com/opencontainers/runc v1.0.0-rc92/go.mod h1:X1zlU4p7wOlX4+WRCz+hvlRv8phdL7UqbYD+vQwNMmE=
|
||||
github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/runtime-spec v1.0.3-0.20200728170252-4d89ac9fbff6 h1:NhsM2gc769rVWDqJvapK37r+7+CBXI8xHhnfnt8uQsg=
|
||||
github.com/opencontainers/runtime-spec v1.0.3-0.20200728170252-4d89ac9fbff6/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/selinux v1.6.0 h1:+bIAS/Za3q5FTwWym4fTB0vObnfCf3G/NC7K6Jx62mY=
|
||||
github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE=
|
||||
github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/seccomp/libseccomp-golang v0.9.1 h1:NJjM5DNFOs0s3kYE1WUOr6G8V97sdt46rlXTMfXGWBo=
|
||||
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2 h1:b6uOv7YOFK0TYG7HtkIgExQo+2RdLuwRft63jn2HWj8=
|
||||
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||
github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY=
|
||||
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=
|
||||
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
|
||||
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k=
|
||||
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
|
||||
github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243 h1:R43TdZy32XXSXjJn7M/HhALJ9imq6ztLnChfYJpVDnM=
|
||||
github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 h1:IaQbIIB2X/Mp/DKctl6ROxz1KyMlKp4uyvL6+kQ7C88=
|
||||
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ymfAeJIyd0upUIElB+lI=
|
||||
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1 h1:sIky/MyNRSHTrdxfsiUSS4WIAMvInbeXljJz+jDjeYE=
|
||||
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E=
|
||||
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
148
grid.go
148
grid.go
@@ -2,10 +2,48 @@ package main
|
||||
|
||||
import (
|
||||
"github.com/bcicen/ctop/config"
|
||||
"github.com/bcicen/ctop/cwidgets/expanded"
|
||||
"github.com/bcicen/ctop/cwidgets/single"
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
func ShowConnError(err error) (exit bool) {
|
||||
ui.Clear()
|
||||
ui.DefaultEvtStream.ResetHandlers()
|
||||
defer ui.DefaultEvtStream.ResetHandlers()
|
||||
|
||||
setErr := func(err error) {
|
||||
errView.Append(err.Error())
|
||||
errView.Append("attempting to reconnect...")
|
||||
ui.Render(errView)
|
||||
}
|
||||
|
||||
HandleKeys("exit", func() {
|
||||
exit = true
|
||||
ui.StopLoop()
|
||||
})
|
||||
|
||||
ui.Handle("/timer/1s", func(ui.Event) {
|
||||
_, err := cursor.RefreshContainers()
|
||||
if err == nil {
|
||||
ui.StopLoop()
|
||||
return
|
||||
}
|
||||
setErr(err)
|
||||
})
|
||||
|
||||
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
|
||||
errView.Resize()
|
||||
ui.Clear()
|
||||
ui.Render(errView)
|
||||
log.Infof("RESIZE")
|
||||
})
|
||||
|
||||
errView.Resize()
|
||||
setErr(err)
|
||||
ui.Loop()
|
||||
return exit
|
||||
}
|
||||
|
||||
func RedrawRows(clr bool) {
|
||||
// reinit body rows
|
||||
cGrid.Clear()
|
||||
@@ -17,6 +55,7 @@ func RedrawRows(clr bool) {
|
||||
header.SetFilter(config.GetVal("filterStr"))
|
||||
y += header.Height()
|
||||
}
|
||||
|
||||
cGrid.SetY(y)
|
||||
|
||||
for _, c := range cursor.filtered {
|
||||
@@ -34,12 +73,17 @@ func RedrawRows(clr bool) {
|
||||
ui.Render(cGrid)
|
||||
}
|
||||
|
||||
func ExpandView(c *Container) {
|
||||
func SingleView() MenuFn {
|
||||
c := cursor.Selected()
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ui.Clear()
|
||||
ui.DefaultEvtStream.ResetHandlers()
|
||||
defer ui.DefaultEvtStream.ResetHandlers()
|
||||
|
||||
ex := expanded.NewExpanded(c.Id)
|
||||
ex := single.NewSingle(c.Id)
|
||||
c.SetUpdater(ex)
|
||||
|
||||
ex.Align()
|
||||
@@ -58,27 +102,40 @@ func ExpandView(c *Container) {
|
||||
|
||||
ui.Loop()
|
||||
c.SetUpdater(c.Widgets)
|
||||
return nil
|
||||
}
|
||||
|
||||
func RefreshDisplay() {
|
||||
needsClear := cursor.RefreshContainers()
|
||||
RedrawRows(needsClear)
|
||||
func RefreshDisplay() error {
|
||||
// skip display refresh during scroll
|
||||
if !cursor.isScrolling {
|
||||
needsClear, err := cursor.RefreshContainers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
RedrawRows(needsClear)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Display() bool {
|
||||
var menu func()
|
||||
var expand bool
|
||||
var menu MenuFn
|
||||
var connErr error
|
||||
|
||||
cGrid.SetWidth(ui.TermWidth())
|
||||
ui.DefaultEvtStream.Hook(logEvent)
|
||||
|
||||
// initial draw
|
||||
header.Align()
|
||||
status.Align()
|
||||
cursor.RefreshContainers()
|
||||
RedrawRows(true)
|
||||
|
||||
HandleKeys("up", cursor.Up)
|
||||
HandleKeys("down", cursor.Down)
|
||||
|
||||
HandleKeys("pgup", cursor.PgUp)
|
||||
HandleKeys("pgdown", cursor.PgDown)
|
||||
|
||||
HandleKeys("exit", ui.StopLoop)
|
||||
HandleKeys("help", func() {
|
||||
menu = HelpMenu
|
||||
@@ -86,12 +143,35 @@ func Display() bool {
|
||||
})
|
||||
|
||||
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
|
||||
expand = true
|
||||
menu = ContainerMenu
|
||||
ui.StopLoop()
|
||||
})
|
||||
ui.Handle("/sys/kbd/<left>", func(ui.Event) {
|
||||
menu = LogMenu
|
||||
ui.StopLoop()
|
||||
})
|
||||
ui.Handle("/sys/kbd/<right>", func(ui.Event) {
|
||||
menu = SingleView
|
||||
ui.StopLoop()
|
||||
})
|
||||
ui.Handle("/sys/kbd/l", func(ui.Event) {
|
||||
menu = LogMenu
|
||||
ui.StopLoop()
|
||||
})
|
||||
ui.Handle("/sys/kbd/e", func(ui.Event) {
|
||||
menu = ExecShell
|
||||
ui.StopLoop()
|
||||
})
|
||||
ui.Handle("/sys/kbd/o", func(ui.Event) {
|
||||
menu = SingleView
|
||||
ui.StopLoop()
|
||||
})
|
||||
ui.Handle("/sys/kbd/a", func(ui.Event) {
|
||||
config.Toggle("allContainers")
|
||||
RefreshDisplay()
|
||||
connErr = RefreshDisplay()
|
||||
if connErr != nil {
|
||||
ui.StopLoop()
|
||||
}
|
||||
})
|
||||
ui.Handle("/sys/kbd/D", func(ui.Event) {
|
||||
dumpContainer(cursor.Selected())
|
||||
@@ -111,13 +191,33 @@ func Display() bool {
|
||||
menu = SortMenu
|
||||
ui.StopLoop()
|
||||
})
|
||||
ui.Handle("/sys/kbd/c", func(ui.Event) {
|
||||
menu = ColumnsMenu
|
||||
ui.StopLoop()
|
||||
})
|
||||
ui.Handle("/sys/kbd/S", func(ui.Event) {
|
||||
path, err := config.Write()
|
||||
if err == nil {
|
||||
log.Statusf("wrote config to %s", path)
|
||||
} else {
|
||||
log.StatusErr(err)
|
||||
}
|
||||
ui.StopLoop()
|
||||
})
|
||||
|
||||
ui.Handle("/timer/1s", func(e ui.Event) {
|
||||
RefreshDisplay()
|
||||
if log.StatusQueued() {
|
||||
ui.StopLoop()
|
||||
}
|
||||
connErr = RefreshDisplay()
|
||||
if connErr != nil {
|
||||
ui.StopLoop()
|
||||
}
|
||||
})
|
||||
|
||||
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
|
||||
header.Align()
|
||||
status.Align()
|
||||
cursor.ScrollPage()
|
||||
cGrid.SetWidth(ui.TermWidth())
|
||||
log.Infof("resize: width=%v max-rows=%v", cGrid.Width, cGrid.MaxRows())
|
||||
@@ -125,16 +225,28 @@ func Display() bool {
|
||||
})
|
||||
|
||||
ui.Loop()
|
||||
if menu != nil {
|
||||
menu()
|
||||
return false
|
||||
|
||||
if connErr != nil {
|
||||
return ShowConnError(connErr)
|
||||
}
|
||||
if expand {
|
||||
c := cursor.Selected()
|
||||
if c != nil {
|
||||
ExpandView(c)
|
||||
|
||||
if log.StatusQueued() {
|
||||
for sm := range log.FlushStatus() {
|
||||
if sm.IsError {
|
||||
status.ShowErr(sm.Text)
|
||||
} else {
|
||||
status.Show(sm.Text)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if menu != nil {
|
||||
for menu != nil {
|
||||
menu = menu()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
84
install.sh
Executable file
84
install.sh
Executable file
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env bash
|
||||
# a simple install script for ctop
|
||||
|
||||
KERNEL=$(uname -s)
|
||||
|
||||
function output() { echo -e "\033[32mctop-install\033[0m $@"; }
|
||||
|
||||
function command_exists() {
|
||||
command -v "$@" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
# extract github download url matching pattern
|
||||
function extract_url() {
|
||||
match=$1; shift
|
||||
echo "$@" | while read line; do
|
||||
case $line in
|
||||
*browser_download_url*${match}*)
|
||||
url=$(echo $line | sed -e 's/^.*"browser_download_url":[ ]*"//' -e 's/".*//;s/\ //g')
|
||||
echo $url
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
case $KERNEL in
|
||||
Linux) MATCH_BUILD="linux-amd64" ;;
|
||||
Darwin) MATCH_BUILD="darwin-amd64" ;;
|
||||
*)
|
||||
echo "platform not supported by this install script"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
for req in curl wget; do
|
||||
command_exists $req || {
|
||||
output "missing required $req binary"
|
||||
req_failed=1
|
||||
}
|
||||
done
|
||||
[ "$req_failed" == 1 ] && exit 1
|
||||
|
||||
sh_c='sh -c'
|
||||
if [ "$CURRENT_USER" != 'root' ]; then
|
||||
if command_exists sudo; then
|
||||
sh_c='sudo -E sh -c'
|
||||
elif command_exists su; then
|
||||
sh_c='su -c'
|
||||
else
|
||||
output "Error: this installer needs the ability to run commands as root. We are unable to find either "sudo" or "su" available to make this happen."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
TMP=$(mktemp -d "${TMPDIR:-/tmp}/ctop.XXXXX")
|
||||
cd ${TMP}
|
||||
|
||||
output "fetching latest release info"
|
||||
resp=$(curl -s https://api.github.com/repos/bcicen/ctop/releases/latest)
|
||||
|
||||
output "fetching release checksums"
|
||||
checksum_url=$(extract_url sha256sums.txt "$resp")
|
||||
wget -q $checksum_url -O sha256sums.txt
|
||||
|
||||
# skip if latest already installed
|
||||
cur_ctop=$(which ctop 2> /dev/null)
|
||||
if [[ -n "$cur_ctop" ]]; then
|
||||
cur_sum=$(sha256sum $cur_ctop | sed 's/ .*//')
|
||||
(grep -q $cur_sum sha256sums.txt) && {
|
||||
output "already up-to-date"
|
||||
exit 0
|
||||
}
|
||||
fi
|
||||
|
||||
output "fetching latest ctop"
|
||||
url=$(extract_url $MATCH_BUILD "$resp")
|
||||
wget -q --show-progress $url
|
||||
(sha256sum -c --quiet --ignore-missing sha256sums.txt) || exit 1
|
||||
|
||||
output "installing to /usr/local/bin"
|
||||
chmod +x ctop-*
|
||||
$sh_c "mv ctop-* /usr/local/bin/ctop"
|
||||
|
||||
output "done!"
|
||||
9
keys.go
9
keys.go
@@ -14,9 +14,18 @@ var keyMap = map[string][]string{
|
||||
"/sys/kbd/<down>",
|
||||
"/sys/kbd/j",
|
||||
},
|
||||
"pgup": []string{
|
||||
"/sys/kbd/<previous>",
|
||||
"/sys/kbd/C-<up>",
|
||||
},
|
||||
"pgdown": []string{
|
||||
"/sys/kbd/<next>",
|
||||
"/sys/kbd/C-<down>",
|
||||
},
|
||||
"exit": []string{
|
||||
"/sys/kbd/q",
|
||||
"/sys/kbd/C-c",
|
||||
"/sys/kbd/<escape>",
|
||||
},
|
||||
"help": []string{
|
||||
"/sys/kbd/h",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
@@ -20,11 +21,36 @@ var (
|
||||
)
|
||||
)
|
||||
|
||||
type statusMsg struct {
|
||||
Text string
|
||||
IsError bool
|
||||
}
|
||||
|
||||
type CTopLogger struct {
|
||||
*logging.Logger
|
||||
backend *logging.MemoryBackend
|
||||
sLog []statusMsg
|
||||
}
|
||||
|
||||
func (c *CTopLogger) FlushStatus() chan statusMsg {
|
||||
ch := make(chan statusMsg)
|
||||
go func() {
|
||||
for _, sm := range c.sLog {
|
||||
ch <- sm
|
||||
}
|
||||
close(ch)
|
||||
c.sLog = []statusMsg{}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
func (c *CTopLogger) StatusQueued() bool { return len(c.sLog) > 0 }
|
||||
func (c *CTopLogger) Status(s string) { c.addStatus(statusMsg{s, false}) }
|
||||
func (c *CTopLogger) StatusErr(err error) { c.addStatus(statusMsg{err.Error(), true}) }
|
||||
func (c *CTopLogger) addStatus(sm statusMsg) { c.sLog = append(c.sLog, sm) }
|
||||
|
||||
func (c *CTopLogger) Statusf(s string, a ...interface{}) { c.Status(fmt.Sprintf(s, a...)) }
|
||||
|
||||
func Init() *CTopLogger {
|
||||
if Log == nil {
|
||||
logging.SetFormatter(format) // setup default formatter
|
||||
@@ -32,6 +58,7 @@ func Init() *CTopLogger {
|
||||
Log = &CTopLogger{
|
||||
logging.MustGetLogger("ctop"),
|
||||
logging.NewMemoryBackend(size),
|
||||
[]statusMsg{},
|
||||
}
|
||||
|
||||
if debugMode() {
|
||||
@@ -78,4 +105,5 @@ func (log *CTopLogger) Exit() {
|
||||
StopServer()
|
||||
}
|
||||
|
||||
func debugMode() bool { return os.Getenv("CTOP_DEBUG") == "1" }
|
||||
func debugMode() bool { return os.Getenv("CTOP_DEBUG") == "1" }
|
||||
func debugModeTCP() bool { return os.Getenv("CTOP_DEBUG_TCP") == "1" }
|
||||
|
||||
@@ -2,12 +2,14 @@ package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
path = "/tmp/ctop.sock"
|
||||
socketPath = "./ctop.sock"
|
||||
socketAddr = "0.0.0.0:9000"
|
||||
)
|
||||
|
||||
var server struct {
|
||||
@@ -16,7 +18,13 @@ var server struct {
|
||||
}
|
||||
|
||||
func getListener() net.Listener {
|
||||
ln, err := net.Listen("unix", path)
|
||||
var ln net.Listener
|
||||
var err error
|
||||
if debugModeTCP() {
|
||||
ln, err = net.Listen("tcp", socketAddr)
|
||||
} else {
|
||||
ln, err = net.Listen("unix", socketPath)
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -49,13 +57,13 @@ func StopServer() {
|
||||
}
|
||||
}
|
||||
|
||||
func handler(conn net.Conn) {
|
||||
func handler(wc io.WriteCloser) {
|
||||
server.wg.Add(1)
|
||||
defer server.wg.Done()
|
||||
defer conn.Close()
|
||||
defer wc.Close()
|
||||
for msg := range Log.tail() {
|
||||
msg = fmt.Sprintf("%s\n", msg)
|
||||
conn.Write([]byte(msg))
|
||||
wc.Write([]byte(msg))
|
||||
}
|
||||
conn.Write([]byte("bye\n"))
|
||||
wc.Write([]byte("bye\n"))
|
||||
}
|
||||
|
||||
92
main.go
92
main.go
@@ -4,39 +4,54 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/bcicen/ctop/config"
|
||||
"github.com/bcicen/ctop/connector"
|
||||
"github.com/bcicen/ctop/container"
|
||||
"github.com/bcicen/ctop/cwidgets/compact"
|
||||
"github.com/bcicen/ctop/logging"
|
||||
"github.com/bcicen/ctop/widgets"
|
||||
ui "github.com/gizak/termui"
|
||||
tm "github.com/nsf/termbox-go"
|
||||
)
|
||||
|
||||
var (
|
||||
build = "none"
|
||||
version = "dev-build"
|
||||
build = "none"
|
||||
version = "dev-build"
|
||||
goVersion = runtime.Version()
|
||||
|
||||
log *logging.CTopLogger
|
||||
cursor *GridCursor
|
||||
cGrid *compact.CompactGrid
|
||||
header *widgets.CTopHeader
|
||||
log *logging.CTopLogger
|
||||
cursor *GridCursor
|
||||
cGrid *compact.CompactGrid
|
||||
header *widgets.CTopHeader
|
||||
status *widgets.StatusLine
|
||||
errView *widgets.ErrorView
|
||||
|
||||
versionStr = fmt.Sprintf("ctop version %v, build %v %v", version, build, goVersion)
|
||||
)
|
||||
|
||||
func main() {
|
||||
defer panicExit()
|
||||
|
||||
// parse command line arguments
|
||||
var versionFlag = flag.Bool("v", false, "output version information and exit")
|
||||
var helpFlag = flag.Bool("h", false, "display this help dialog")
|
||||
var filterFlag = flag.String("f", "", "filter containers")
|
||||
var activeOnlyFlag = flag.Bool("a", false, "show active containers only")
|
||||
var sortFieldFlag = flag.String("s", "", "select container sort field")
|
||||
var reverseSortFlag = flag.Bool("r", false, "reverse container sort order")
|
||||
var invertFlag = flag.Bool("i", false, "invert default colors")
|
||||
var (
|
||||
versionFlag = flag.Bool("v", false, "output version information and exit")
|
||||
helpFlag = flag.Bool("h", false, "display this help dialog")
|
||||
filterFlag = flag.String("f", "", "filter containers")
|
||||
activeOnlyFlag = flag.Bool("a", false, "show active containers only")
|
||||
sortFieldFlag = flag.String("s", "", "select container sort field")
|
||||
reverseSortFlag = flag.Bool("r", false, "reverse container sort order")
|
||||
invertFlag = flag.Bool("i", false, "invert default colors")
|
||||
scaleCpu = flag.Bool("scale-cpu", false, "show cpu as % of system total")
|
||||
connectorFlag = flag.String("connector", "docker", "container connector to use")
|
||||
defaultShell = flag.String("shell", "sh", "exec shell to use")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
if *versionFlag {
|
||||
printVersion()
|
||||
fmt.Println(versionStr)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
@@ -48,8 +63,11 @@ func main() {
|
||||
// init logger
|
||||
log = logging.Init()
|
||||
|
||||
// init global config
|
||||
// init global config and read config file if exists
|
||||
config.Init()
|
||||
if err := config.Read(); err != nil {
|
||||
log.Warningf("reading config: %s", err)
|
||||
}
|
||||
|
||||
// override default config values with command line flags
|
||||
if *filterFlag != "" {
|
||||
@@ -69,6 +87,14 @@ func main() {
|
||||
config.Toggle("sortReversed")
|
||||
}
|
||||
|
||||
if *scaleCpu {
|
||||
config.Toggle("scaleCpu")
|
||||
}
|
||||
|
||||
if *defaultShell != "" {
|
||||
config.Update("shell", *defaultShell)
|
||||
}
|
||||
|
||||
// init ui
|
||||
if *invertFlag {
|
||||
InvertColorMap()
|
||||
@@ -77,26 +103,39 @@ func main() {
|
||||
if err := ui.Init(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer ui.Close()
|
||||
tm.SetInputMode(tm.InputAlt)
|
||||
|
||||
defer Shutdown()
|
||||
// init grid, cursor, header
|
||||
cursor = NewGridCursor()
|
||||
cSuper, err := connector.ByName(*connectorFlag)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cursor = &GridCursor{cSuper: cSuper}
|
||||
cGrid = compact.NewCompactGrid()
|
||||
header = widgets.NewCTopHeader()
|
||||
status = widgets.NewStatusLine()
|
||||
errView = widgets.NewErrorView()
|
||||
|
||||
for {
|
||||
exit := Display()
|
||||
if exit {
|
||||
log.Notice("shutting down")
|
||||
log.Exit()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Shutdown() {
|
||||
log.Notice("shutting down")
|
||||
log.Exit()
|
||||
if tm.IsInit {
|
||||
ui.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// ensure a given sort field is valid
|
||||
func validSort(s string) {
|
||||
if _, ok := Sorters[s]; !ok {
|
||||
if _, ok := container.Sorters[s]; !ok {
|
||||
fmt.Printf("invalid sort field: %s\n", s)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -104,13 +143,14 @@ func validSort(s string) {
|
||||
|
||||
func panicExit() {
|
||||
if r := recover(); r != nil {
|
||||
ui.Clear()
|
||||
fmt.Printf("panic: %s\n", r)
|
||||
Shutdown()
|
||||
panic(r)
|
||||
fmt.Printf("error: %s\n", r)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
var helpMsg = `ctop - container metric viewer
|
||||
var helpMsg = `ctop - interactive container viewer
|
||||
|
||||
usage: ctop [options]
|
||||
|
||||
@@ -120,8 +160,6 @@ options:
|
||||
func printHelp() {
|
||||
fmt.Println(helpMsg)
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
func printVersion() {
|
||||
fmt.Printf("ctop version %v, build %v\n", version, build)
|
||||
fmt.Printf("\navailable connectors: ")
|
||||
fmt.Println(strings.Join(connector.Enabled(), ", "))
|
||||
}
|
||||
|
||||
391
menus.go
391
menus.go
@@ -1,23 +1,38 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bcicen/ctop/config"
|
||||
"github.com/bcicen/ctop/container"
|
||||
"github.com/bcicen/ctop/widgets"
|
||||
"github.com/bcicen/ctop/widgets/menu"
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
// MenuFn executes a menu window, returning the next menu or nil
|
||||
type MenuFn func() MenuFn
|
||||
|
||||
var helpDialog = []menu.Item{
|
||||
menu.Item{"[a] - toggle display of all containers", ""},
|
||||
menu.Item{"[f] - filter displayed containers", ""},
|
||||
menu.Item{"[h] - open this help dialog", ""},
|
||||
menu.Item{"[H] - toggle ctop header", ""},
|
||||
menu.Item{"[s] - select container sort field", ""},
|
||||
menu.Item{"[r] - reverse container sort order", ""},
|
||||
menu.Item{"[q] - exit ctop", ""},
|
||||
{"<enter> - open container menu", ""},
|
||||
{"", ""},
|
||||
{"[a] - toggle display of all containers", ""},
|
||||
{"[f] - filter displayed containers", ""},
|
||||
{"[h] - open this help dialog", ""},
|
||||
{"[H] - toggle ctop header", ""},
|
||||
{"[s] - select container sort field", ""},
|
||||
{"[r] - reverse container sort order", ""},
|
||||
{"[o] - open single view", ""},
|
||||
{"[l] - view container logs ([t] to toggle timestamp when open)", ""},
|
||||
{"[e] - exec shell", ""},
|
||||
{"[c] - configure columns", ""},
|
||||
{"[S] - save current configuration to file", ""},
|
||||
{"[q] - exit ctop", ""},
|
||||
}
|
||||
|
||||
func HelpMenu() {
|
||||
func HelpMenu() MenuFn {
|
||||
ui.Clear()
|
||||
ui.DefaultEvtStream.ResetHandlers()
|
||||
defer ui.DefaultEvtStream.ResetHandlers()
|
||||
@@ -25,14 +40,18 @@ func HelpMenu() {
|
||||
m := menu.NewMenu()
|
||||
m.BorderLabel = "Help"
|
||||
m.AddItems(helpDialog...)
|
||||
ui.Render(m)
|
||||
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
|
||||
ui.Clear()
|
||||
ui.Render(m)
|
||||
})
|
||||
ui.Handle("/sys/kbd/", func(ui.Event) {
|
||||
ui.StopLoop()
|
||||
})
|
||||
ui.Loop()
|
||||
return nil
|
||||
}
|
||||
|
||||
func FilterMenu() {
|
||||
func FilterMenu() MenuFn {
|
||||
ui.DefaultEvtStream.ResetHandlers()
|
||||
defer ui.DefaultEvtStream.ResetHandlers()
|
||||
|
||||
@@ -62,9 +81,10 @@ func FilterMenu() {
|
||||
ui.StopLoop()
|
||||
})
|
||||
ui.Loop()
|
||||
return nil
|
||||
}
|
||||
|
||||
func SortMenu() {
|
||||
func SortMenu() MenuFn {
|
||||
ui.Clear()
|
||||
ui.DefaultEvtStream.ResetHandlers()
|
||||
defer ui.DefaultEvtStream.ResetHandlers()
|
||||
@@ -74,7 +94,7 @@ func SortMenu() {
|
||||
m.SortItems = true
|
||||
m.BorderLabel = "Sort Field"
|
||||
|
||||
for _, field := range SortFields() {
|
||||
for _, field := range container.SortFields() {
|
||||
m.AddItems(menu.Item{field, ""})
|
||||
}
|
||||
|
||||
@@ -86,10 +106,355 @@ func SortMenu() {
|
||||
HandleKeys("exit", ui.StopLoop)
|
||||
|
||||
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
|
||||
config.Update("sortField", m.SelectedItem().Val)
|
||||
config.Update("sortField", m.SelectedValue())
|
||||
ui.StopLoop()
|
||||
})
|
||||
|
||||
ui.Render(m)
|
||||
ui.Loop()
|
||||
return nil
|
||||
}
|
||||
|
||||
func ColumnsMenu() MenuFn {
|
||||
const (
|
||||
enabledStr = "[X]"
|
||||
disabledStr = "[ ]"
|
||||
padding = 2
|
||||
)
|
||||
|
||||
ui.Clear()
|
||||
ui.DefaultEvtStream.ResetHandlers()
|
||||
defer ui.DefaultEvtStream.ResetHandlers()
|
||||
|
||||
m := menu.NewMenu()
|
||||
m.Selectable = true
|
||||
m.SortItems = false
|
||||
m.BorderLabel = "Columns"
|
||||
//m.SubText = "Enabled Columns"
|
||||
|
||||
rebuild := func() {
|
||||
// get padding for right alignment of enabled status
|
||||
var maxLen int
|
||||
for _, col := range config.GlobalColumns {
|
||||
if len(col.Label) > maxLen {
|
||||
maxLen = len(col.Label)
|
||||
}
|
||||
}
|
||||
maxLen += padding
|
||||
|
||||
// rebuild menu items
|
||||
m.ClearItems()
|
||||
for _, col := range config.GlobalColumns {
|
||||
txt := col.Label + strings.Repeat(" ", maxLen-len(col.Label))
|
||||
if col.Enabled {
|
||||
txt += enabledStr
|
||||
} else {
|
||||
txt += disabledStr
|
||||
}
|
||||
m.AddItems(menu.Item{col.Name, txt})
|
||||
}
|
||||
}
|
||||
|
||||
upFn := func() {
|
||||
config.ColumnLeft(m.SelectedValue())
|
||||
m.Up()
|
||||
rebuild()
|
||||
}
|
||||
|
||||
downFn := func() {
|
||||
config.ColumnRight(m.SelectedValue())
|
||||
m.Down()
|
||||
rebuild()
|
||||
}
|
||||
|
||||
toggleFn := func() {
|
||||
config.ColumnToggle(m.SelectedValue())
|
||||
rebuild()
|
||||
}
|
||||
|
||||
rebuild()
|
||||
|
||||
HandleKeys("up", m.Up)
|
||||
HandleKeys("down", m.Down)
|
||||
HandleKeys("enter", toggleFn)
|
||||
HandleKeys("pgup", upFn)
|
||||
HandleKeys("pgdown", downFn)
|
||||
|
||||
ui.Handle("/sys/kbd/x", func(ui.Event) { toggleFn() })
|
||||
ui.Handle("/sys/kbd/<enter>", func(ui.Event) { toggleFn() })
|
||||
|
||||
HandleKeys("exit", func() {
|
||||
cSource, err := cursor.cSuper.Get()
|
||||
if err == nil {
|
||||
for _, c := range cSource.All() {
|
||||
c.RecreateWidgets()
|
||||
}
|
||||
}
|
||||
ui.StopLoop()
|
||||
})
|
||||
|
||||
ui.Render(m)
|
||||
ui.Loop()
|
||||
return nil
|
||||
}
|
||||
|
||||
func ContainerMenu() MenuFn {
|
||||
c := cursor.Selected()
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ui.DefaultEvtStream.ResetHandlers()
|
||||
defer ui.DefaultEvtStream.ResetHandlers()
|
||||
|
||||
m := menu.NewMenu()
|
||||
m.Selectable = true
|
||||
m.BorderLabel = "Menu"
|
||||
|
||||
items := []menu.Item{
|
||||
menu.Item{Val: "single", Label: "[o] single view"},
|
||||
menu.Item{Val: "logs", Label: "[l] log view"},
|
||||
}
|
||||
|
||||
if c.Meta["state"] == "running" {
|
||||
items = append(items, menu.Item{Val: "stop", Label: "[s] stop"})
|
||||
items = append(items, menu.Item{Val: "pause", Label: "[p] pause"})
|
||||
items = append(items, menu.Item{Val: "restart", Label: "[r] restart"})
|
||||
items = append(items, menu.Item{Val: "exec", Label: "[e] exec shell"})
|
||||
}
|
||||
if c.Meta["state"] == "exited" || c.Meta["state"] == "created" {
|
||||
items = append(items, menu.Item{Val: "start", Label: "[s] start"})
|
||||
items = append(items, menu.Item{Val: "remove", Label: "[R] remove"})
|
||||
}
|
||||
if c.Meta["state"] == "paused" {
|
||||
items = append(items, menu.Item{Val: "unpause", Label: "[p] unpause"})
|
||||
}
|
||||
items = append(items, menu.Item{Val: "cancel", Label: "[c] cancel"})
|
||||
|
||||
m.AddItems(items...)
|
||||
ui.Render(m)
|
||||
|
||||
HandleKeys("up", m.Up)
|
||||
HandleKeys("down", m.Down)
|
||||
|
||||
var selected string
|
||||
|
||||
// shortcuts
|
||||
ui.Handle("/sys/kbd/o", func(ui.Event) {
|
||||
selected = "single"
|
||||
ui.StopLoop()
|
||||
})
|
||||
ui.Handle("/sys/kbd/l", func(ui.Event) {
|
||||
selected = "logs"
|
||||
ui.StopLoop()
|
||||
})
|
||||
if c.Meta["state"] != "paused" {
|
||||
ui.Handle("/sys/kbd/s", func(ui.Event) {
|
||||
if c.Meta["state"] == "running" {
|
||||
selected = "stop"
|
||||
} else {
|
||||
selected = "start"
|
||||
}
|
||||
ui.StopLoop()
|
||||
})
|
||||
}
|
||||
if c.Meta["state"] != "exited" || c.Meta["state"] != "created" {
|
||||
ui.Handle("/sys/kbd/p", func(ui.Event) {
|
||||
if c.Meta["state"] == "paused" {
|
||||
selected = "unpause"
|
||||
} else {
|
||||
selected = "pause"
|
||||
}
|
||||
ui.StopLoop()
|
||||
})
|
||||
}
|
||||
if c.Meta["state"] == "running" {
|
||||
ui.Handle("/sys/kbd/e", func(ui.Event) {
|
||||
selected = "exec"
|
||||
ui.StopLoop()
|
||||
})
|
||||
ui.Handle("/sys/kbd/r", func(ui.Event) {
|
||||
selected = "restart"
|
||||
ui.StopLoop()
|
||||
})
|
||||
}
|
||||
ui.Handle("/sys/kbd/R", func(ui.Event) {
|
||||
selected = "remove"
|
||||
ui.StopLoop()
|
||||
})
|
||||
ui.Handle("/sys/kbd/c", func(ui.Event) {
|
||||
ui.StopLoop()
|
||||
})
|
||||
|
||||
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
|
||||
selected = m.SelectedValue()
|
||||
ui.StopLoop()
|
||||
})
|
||||
ui.Handle("/sys/kbd/", func(ui.Event) {
|
||||
ui.StopLoop()
|
||||
})
|
||||
ui.Loop()
|
||||
|
||||
var nextMenu MenuFn
|
||||
switch selected {
|
||||
case "single":
|
||||
nextMenu = SingleView
|
||||
case "logs":
|
||||
nextMenu = LogMenu
|
||||
case "exec":
|
||||
nextMenu = ExecShell
|
||||
case "start":
|
||||
nextMenu = Confirm(confirmTxt("start", c.GetMeta("name")), c.Start)
|
||||
case "stop":
|
||||
nextMenu = Confirm(confirmTxt("stop", c.GetMeta("name")), c.Stop)
|
||||
case "remove":
|
||||
nextMenu = Confirm(confirmTxt("remove", c.GetMeta("name")), c.Remove)
|
||||
case "pause":
|
||||
nextMenu = Confirm(confirmTxt("pause", c.GetMeta("name")), c.Pause)
|
||||
case "unpause":
|
||||
nextMenu = Confirm(confirmTxt("unpause", c.GetMeta("name")), c.Unpause)
|
||||
case "restart":
|
||||
nextMenu = Confirm(confirmTxt("restart", c.GetMeta("name")), c.Restart)
|
||||
}
|
||||
|
||||
return nextMenu
|
||||
}
|
||||
|
||||
func LogMenu() MenuFn {
|
||||
|
||||
c := cursor.Selected()
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ui.DefaultEvtStream.ResetHandlers()
|
||||
defer ui.DefaultEvtStream.ResetHandlers()
|
||||
|
||||
logs, quit := logReader(c)
|
||||
m := widgets.NewTextView(logs)
|
||||
m.BorderLabel = fmt.Sprintf("Logs [%s]", c.GetMeta("name"))
|
||||
ui.Render(m)
|
||||
|
||||
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
|
||||
m.Resize()
|
||||
})
|
||||
ui.Handle("/sys/kbd/t", func(ui.Event) {
|
||||
m.Toggle()
|
||||
})
|
||||
ui.Handle("/sys/kbd/", func(ui.Event) {
|
||||
quit <- true
|
||||
ui.StopLoop()
|
||||
})
|
||||
ui.Loop()
|
||||
return nil
|
||||
}
|
||||
|
||||
func ExecShell() MenuFn {
|
||||
c := cursor.Selected()
|
||||
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ui.DefaultEvtStream.ResetHandlers()
|
||||
defer ui.DefaultEvtStream.ResetHandlers()
|
||||
|
||||
shell := config.Get("shell")
|
||||
if err := c.Exec([]string{shell.Val, "-c", "printf '\\e[0m\\e[?25h' && clear && " + shell.Val}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a confirmation dialog with a given description string and
|
||||
// func to perform if confirmed
|
||||
func Confirm(txt string, fn func()) MenuFn {
|
||||
menu := func() MenuFn {
|
||||
ui.DefaultEvtStream.ResetHandlers()
|
||||
defer ui.DefaultEvtStream.ResetHandlers()
|
||||
|
||||
m := menu.NewMenu()
|
||||
m.Selectable = true
|
||||
m.BorderLabel = "Confirm"
|
||||
m.SubText = txt
|
||||
|
||||
items := []menu.Item{
|
||||
menu.Item{Val: "cancel", Label: "[c]ancel"},
|
||||
menu.Item{Val: "yes", Label: "[y]es"},
|
||||
}
|
||||
|
||||
var response bool
|
||||
|
||||
m.AddItems(items...)
|
||||
ui.Render(m)
|
||||
|
||||
yes := func() {
|
||||
response = true
|
||||
ui.StopLoop()
|
||||
}
|
||||
|
||||
no := func() {
|
||||
response = false
|
||||
ui.StopLoop()
|
||||
}
|
||||
|
||||
HandleKeys("up", m.Up)
|
||||
HandleKeys("down", m.Down)
|
||||
HandleKeys("exit", no)
|
||||
ui.Handle("/sys/kbd/c", func(ui.Event) { no() })
|
||||
ui.Handle("/sys/kbd/y", func(ui.Event) { yes() })
|
||||
|
||||
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
|
||||
switch m.SelectedValue() {
|
||||
case "cancel":
|
||||
no()
|
||||
case "yes":
|
||||
yes()
|
||||
}
|
||||
})
|
||||
|
||||
ui.Loop()
|
||||
if response {
|
||||
fn()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return menu
|
||||
}
|
||||
|
||||
type toggleLog struct {
|
||||
timestamp time.Time
|
||||
message string
|
||||
}
|
||||
|
||||
func (t *toggleLog) Toggle(on bool) string {
|
||||
if on {
|
||||
return fmt.Sprintf("%s %s", t.timestamp.Format("2006-01-02T15:04:05.999Z07:00"), t.message)
|
||||
}
|
||||
return t.message
|
||||
}
|
||||
|
||||
func logReader(container *container.Container) (logs chan widgets.ToggleText, quit chan bool) {
|
||||
|
||||
logCollector := container.Logs()
|
||||
stream := logCollector.Stream()
|
||||
logs = make(chan widgets.ToggleText)
|
||||
quit = make(chan bool)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case log := <-stream:
|
||||
logs <- &toggleLog{timestamp: log.Timestamp, message: log.Message}
|
||||
case <-quit:
|
||||
logCollector.Stop()
|
||||
close(logs)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
return
|
||||
}
|
||||
|
||||
func confirmTxt(a, n string) string { return fmt.Sprintf("%s container %s?", a, n) }
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/bcicen/ctop/logging"
|
||||
)
|
||||
|
||||
var log = logging.Init()
|
||||
|
||||
type Metrics struct {
|
||||
CPUUtil int
|
||||
NetTx int64
|
||||
NetRx int64
|
||||
MemLimit int64
|
||||
MemPercent int
|
||||
MemUsage int64
|
||||
IOBytesRead int64
|
||||
IOBytesWrite int64
|
||||
Pids int
|
||||
}
|
||||
|
||||
func NewMetrics() Metrics {
|
||||
return Metrics{
|
||||
CPUUtil: -1,
|
||||
NetTx: -1,
|
||||
NetRx: -1,
|
||||
MemUsage: -1,
|
||||
MemPercent: -1,
|
||||
IOBytesRead: -1,
|
||||
IOBytesWrite: -1,
|
||||
Pids: -1,
|
||||
}
|
||||
}
|
||||
|
||||
type Collector interface {
|
||||
Stream() chan Metrics
|
||||
Running() bool
|
||||
Start()
|
||||
Stop()
|
||||
}
|
||||
|
||||
func round(num float64) int {
|
||||
return int(num + math.Copysign(0.5, num))
|
||||
}
|
||||
56
models/main.go
Normal file
56
models/main.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type Log struct {
|
||||
Timestamp time.Time
|
||||
Message string
|
||||
}
|
||||
|
||||
type Meta map[string]string
|
||||
|
||||
// NewMeta returns an initialized Meta map.
|
||||
// An optional series of key, values may be provided to populate the map prior to returning
|
||||
func NewMeta(kvs ...string) Meta {
|
||||
m := make(Meta)
|
||||
|
||||
var i int
|
||||
for i < len(kvs)-1 {
|
||||
m[kvs[i]] = kvs[i+1]
|
||||
i += 2
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (m Meta) Get(k string) string {
|
||||
if s, ok := m[k]; ok {
|
||||
return s
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type Metrics struct {
|
||||
CPUUtil int
|
||||
NetTx int64
|
||||
NetRx int64
|
||||
MemLimit int64
|
||||
MemPercent int
|
||||
MemUsage int64
|
||||
IOBytesRead int64
|
||||
IOBytesWrite int64
|
||||
Pids int
|
||||
}
|
||||
|
||||
func NewMetrics() Metrics {
|
||||
return Metrics{
|
||||
CPUUtil: -1,
|
||||
NetTx: -1,
|
||||
NetRx: -1,
|
||||
MemUsage: -1,
|
||||
MemPercent: -1,
|
||||
IOBytesRead: -1,
|
||||
IOBytesWrite: -1,
|
||||
Pids: -1,
|
||||
}
|
||||
}
|
||||
60
widgets/error.go
Normal file
60
widgets/error.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package widgets
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
type ErrorView struct {
|
||||
*ui.Par
|
||||
lines []string
|
||||
}
|
||||
|
||||
func NewErrorView() *ErrorView {
|
||||
const yPad = 1
|
||||
const xPad = 2
|
||||
|
||||
p := ui.NewPar("")
|
||||
p.X = xPad
|
||||
p.Y = yPad
|
||||
p.Border = true
|
||||
p.Height = 10
|
||||
p.Width = 20
|
||||
p.PaddingTop = yPad
|
||||
p.PaddingBottom = yPad
|
||||
p.PaddingLeft = xPad
|
||||
p.PaddingRight = xPad
|
||||
p.BorderLabel = " ctop - error "
|
||||
p.Bg = ui.ThemeAttr("bg")
|
||||
p.TextFgColor = ui.ThemeAttr("status.warn")
|
||||
p.TextBgColor = ui.ThemeAttr("menu.text.bg")
|
||||
p.BorderFg = ui.ThemeAttr("status.warn")
|
||||
p.BorderLabelFg = ui.ThemeAttr("status.warn")
|
||||
return &ErrorView{p, make([]string, 0, 50)}
|
||||
}
|
||||
|
||||
func (w *ErrorView) Append(s string) {
|
||||
if len(w.lines)+2 >= cap(w.lines) {
|
||||
w.lines = append(w.lines[:0], w.lines[2:]...)
|
||||
}
|
||||
ts := time.Now().Local().Format("15:04:05 MST")
|
||||
w.lines = append(w.lines, fmt.Sprintf("[%s] %s", ts, s))
|
||||
w.lines = append(w.lines, "")
|
||||
}
|
||||
|
||||
func (w *ErrorView) Buffer() ui.Buffer {
|
||||
offset := len(w.lines) - w.InnerHeight()
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
w.Text = strings.Join(w.lines[offset:len(w.lines)], "\n")
|
||||
return w.Par.Buffer()
|
||||
}
|
||||
|
||||
func (w *ErrorView) Resize() {
|
||||
w.Height = ui.TermHeight() - (w.PaddingTop + w.PaddingBottom)
|
||||
w.SetWidth(ui.TermWidth() - (w.PaddingLeft + w.PaddingRight))
|
||||
}
|
||||
@@ -16,9 +16,9 @@ type CTopHeader struct {
|
||||
|
||||
func NewCTopHeader() *CTopHeader {
|
||||
return &CTopHeader{
|
||||
Time: headerPar(2, timeStr()),
|
||||
Count: headerPar(27, "-"),
|
||||
Filter: headerPar(47, ""),
|
||||
Time: headerPar(2, ""),
|
||||
Count: headerPar(24, "-"),
|
||||
Filter: headerPar(40, ""),
|
||||
bg: headerBg(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ func (i *Input) KeyPress(e ui.Event) {
|
||||
if len(i.Data) >= i.MaxLen {
|
||||
return
|
||||
}
|
||||
if strings.Index(input_chars, ch) > -1 {
|
||||
if strings.Contains(input_chars, ch) {
|
||||
i.Data += ch
|
||||
i.stream <- i.Data
|
||||
ui.Render(i)
|
||||
|
||||
@@ -10,13 +10,15 @@ type Padding [2]int // x,y padding
|
||||
|
||||
type Menu struct {
|
||||
ui.Block
|
||||
SortItems bool // enable automatic sorting of menu items
|
||||
SortItems bool // enable automatic sorting of menu items
|
||||
Selectable bool // whether menu is navigable
|
||||
SubText string // optional text to display before items
|
||||
TextFgColor ui.Attribute
|
||||
TextBgColor ui.Attribute
|
||||
Selectable bool
|
||||
cursorPos int
|
||||
items Items
|
||||
padding Padding
|
||||
toolTip *ToolTip
|
||||
}
|
||||
|
||||
func NewMenu() *Menu {
|
||||
@@ -41,7 +43,7 @@ func (m *Menu) AddItems(items ...Item) {
|
||||
m.refresh()
|
||||
}
|
||||
|
||||
// Remove menu item by value or label
|
||||
// DelItem removes menu item by value or label
|
||||
func (m *Menu) DelItem(s string) (success bool) {
|
||||
for n, i := range m.items {
|
||||
if i.Val == s || i.Label == s {
|
||||
@@ -54,6 +56,11 @@ func (m *Menu) DelItem(s string) (success bool) {
|
||||
return success
|
||||
}
|
||||
|
||||
// ClearItems removes all current menu items
|
||||
func (m *Menu) ClearItems() {
|
||||
m.items = m.items[:0]
|
||||
}
|
||||
|
||||
// Move cursor to an position by Item value or label
|
||||
func (m *Menu) SetCursor(s string) (success bool) {
|
||||
for n, i := range m.items {
|
||||
@@ -65,26 +72,36 @@ func (m *Menu) SetCursor(s string) (success bool) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Sort menu items(if enabled) and re-calculate window size
|
||||
func (m *Menu) refresh() {
|
||||
if m.SortItems {
|
||||
sort.Sort(m.items)
|
||||
}
|
||||
m.calcSize()
|
||||
ui.Render(m)
|
||||
// SetToolTip sets an optional tooltip string to show at bottom of screen
|
||||
func (m *Menu) SetToolTip(lines ...string) {
|
||||
m.toolTip = NewToolTip(lines...)
|
||||
}
|
||||
|
||||
func (m *Menu) SelectedItem() Item {
|
||||
return m.items[m.cursorPos]
|
||||
}
|
||||
|
||||
func (m *Menu) SelectedValue() string {
|
||||
return m.items[m.cursorPos].Val
|
||||
}
|
||||
|
||||
func (m *Menu) Buffer() ui.Buffer {
|
||||
var cell ui.Cell
|
||||
buf := m.Block.Buffer()
|
||||
|
||||
y := m.Y + m.padding[1]
|
||||
|
||||
if m.SubText != "" {
|
||||
x := m.X + m.padding[0]
|
||||
for i, ch := range m.SubText {
|
||||
cell = ui.Cell{Ch: ch, Fg: m.TextFgColor, Bg: m.TextBgColor}
|
||||
buf.Set(x+i, y, cell)
|
||||
}
|
||||
y += 2
|
||||
}
|
||||
|
||||
for n, item := range m.items {
|
||||
x := m.X + m.padding[0]
|
||||
y := m.Y + m.padding[1]
|
||||
for _, ch := range item.Text() {
|
||||
// invert bg/fg colors on currently selected row
|
||||
if m.Selectable && n == m.cursorPos {
|
||||
@@ -97,6 +114,10 @@ func (m *Menu) Buffer() ui.Buffer {
|
||||
}
|
||||
}
|
||||
|
||||
if m.toolTip != nil {
|
||||
buf.Merge(m.toolTip.Buffer())
|
||||
}
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
@@ -114,18 +135,35 @@ func (m *Menu) Down() {
|
||||
}
|
||||
}
|
||||
|
||||
// Sort menu items(if enabled) and re-calculate window size
|
||||
func (m *Menu) refresh() {
|
||||
if m.SortItems {
|
||||
sort.Sort(m.items)
|
||||
}
|
||||
m.calcSize()
|
||||
ui.Render(m)
|
||||
}
|
||||
|
||||
// Set width and height based on menu items
|
||||
func (m *Menu) calcSize() {
|
||||
m.Width = 7 // minimum width
|
||||
|
||||
items := m.items
|
||||
var height int
|
||||
for _, i := range m.items {
|
||||
s := i.Text()
|
||||
if len(s) > m.Width {
|
||||
m.Width = len(s)
|
||||
}
|
||||
height++
|
||||
}
|
||||
|
||||
if m.SubText != "" {
|
||||
if len(m.SubText) > m.Width {
|
||||
m.Width = len(m.SubText)
|
||||
}
|
||||
height += 2
|
||||
}
|
||||
|
||||
m.Width += (m.padding[0] * 2)
|
||||
m.Height = len(items) + (m.padding[1] * 2)
|
||||
m.Height = height + (m.padding[1] * 2)
|
||||
}
|
||||
|
||||
55
widgets/menu/tooltip.go
Normal file
55
widgets/menu/tooltip.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
type ToolTip struct {
|
||||
ui.Block
|
||||
Lines []string
|
||||
TextFgColor ui.Attribute
|
||||
TextBgColor ui.Attribute
|
||||
padding Padding
|
||||
}
|
||||
|
||||
func NewToolTip(lines ...string) *ToolTip {
|
||||
t := &ToolTip{
|
||||
Block: *ui.NewBlock(),
|
||||
Lines: lines,
|
||||
TextFgColor: ui.ThemeAttr("menu.text.fg"),
|
||||
TextBgColor: ui.ThemeAttr("menu.text.bg"),
|
||||
padding: Padding{2, 1},
|
||||
}
|
||||
t.BorderFg = ui.ThemeAttr("menu.border.fg")
|
||||
t.BorderLabelFg = ui.ThemeAttr("menu.label.fg")
|
||||
t.X = 1
|
||||
t.Align()
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *ToolTip) Buffer() ui.Buffer {
|
||||
var cell ui.Cell
|
||||
buf := t.Block.Buffer()
|
||||
|
||||
y := t.Y + t.padding[1]
|
||||
|
||||
for n, line := range t.Lines {
|
||||
x := t.X + t.padding[0]
|
||||
for _, ch := range line {
|
||||
cell = ui.Cell{Ch: ch, Fg: t.TextFgColor, Bg: t.TextBgColor}
|
||||
buf.Set(x, y+n, cell)
|
||||
x++
|
||||
}
|
||||
}
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
// Set width and height based on screen size
|
||||
func (t *ToolTip) Align() {
|
||||
t.Width = ui.TermWidth() - (t.padding[0] * 2)
|
||||
t.Height = len(t.Lines) + (t.padding[1] * 2)
|
||||
t.Y = ui.TermHeight() - t.Height
|
||||
|
||||
t.Block.Align()
|
||||
}
|
||||
87
widgets/status.go
Normal file
87
widgets/status.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package widgets
|
||||
|
||||
import (
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
var (
|
||||
statusHeight = 1
|
||||
statusIter = 3
|
||||
)
|
||||
|
||||
type StatusLine struct {
|
||||
Message *ui.Par
|
||||
bg *ui.Par
|
||||
}
|
||||
|
||||
func NewStatusLine() *StatusLine {
|
||||
p := ui.NewPar("")
|
||||
p.X = 2
|
||||
p.Border = false
|
||||
p.Height = statusHeight
|
||||
p.Bg = ui.ThemeAttr("header.bg")
|
||||
p.TextFgColor = ui.ThemeAttr("header.fg")
|
||||
p.TextBgColor = ui.ThemeAttr("header.bg")
|
||||
return &StatusLine{
|
||||
Message: p,
|
||||
bg: statusBg(),
|
||||
}
|
||||
}
|
||||
|
||||
func (sl *StatusLine) Display() {
|
||||
ui.DefaultEvtStream.ResetHandlers()
|
||||
defer ui.DefaultEvtStream.ResetHandlers()
|
||||
|
||||
iter := statusIter
|
||||
ui.Handle("/sys/kbd/", func(ui.Event) {
|
||||
ui.StopLoop()
|
||||
})
|
||||
ui.Handle("/timer/1s", func(ui.Event) {
|
||||
iter--
|
||||
if iter <= 0 {
|
||||
ui.StopLoop()
|
||||
}
|
||||
})
|
||||
|
||||
ui.Render(sl)
|
||||
ui.Loop()
|
||||
}
|
||||
|
||||
// change given message on the status line
|
||||
func (sl *StatusLine) Show(s string) {
|
||||
sl.Message.TextFgColor = ui.ThemeAttr("header.fg")
|
||||
sl.Message.Text = s
|
||||
sl.Display()
|
||||
}
|
||||
|
||||
func (sl *StatusLine) ShowErr(s string) {
|
||||
sl.Message.TextFgColor = ui.ThemeAttr("status.danger")
|
||||
sl.Message.Text = s
|
||||
sl.Display()
|
||||
}
|
||||
|
||||
func (sl *StatusLine) Buffer() ui.Buffer {
|
||||
buf := ui.NewBuffer()
|
||||
buf.Merge(sl.bg.Buffer())
|
||||
buf.Merge(sl.Message.Buffer())
|
||||
return buf
|
||||
}
|
||||
|
||||
func (sl *StatusLine) Align() {
|
||||
sl.bg.SetWidth(ui.TermWidth() - 1)
|
||||
sl.Message.SetWidth(ui.TermWidth() - 2)
|
||||
|
||||
sl.bg.Y = ui.TermHeight() - 1
|
||||
sl.Message.Y = ui.TermHeight() - 1
|
||||
}
|
||||
|
||||
func (sl *StatusLine) Height() int { return statusHeight }
|
||||
|
||||
func statusBg() *ui.Par {
|
||||
bg := ui.NewPar("")
|
||||
bg.X = 1
|
||||
bg.Height = statusHeight
|
||||
bg.Border = false
|
||||
bg.Bg = ui.ThemeAttr("header.bg")
|
||||
return bg
|
||||
}
|
||||
124
widgets/view.go
Normal file
124
widgets/view.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package widgets
|
||||
|
||||
import (
|
||||
ui "github.com/gizak/termui"
|
||||
)
|
||||
|
||||
type ToggleText interface {
|
||||
// returns text for toggle on/off
|
||||
Toggle(on bool) string
|
||||
}
|
||||
|
||||
type TextView struct {
|
||||
ui.Block
|
||||
inputStream <-chan ToggleText
|
||||
render chan bool
|
||||
toggleState bool
|
||||
Text []ToggleText // all the text
|
||||
TextOut []string // text to be displayed
|
||||
TextFgColor ui.Attribute
|
||||
TextBgColor ui.Attribute
|
||||
padding Padding
|
||||
}
|
||||
|
||||
func NewTextView(lines <-chan ToggleText) *TextView {
|
||||
t := &TextView{
|
||||
Block: *ui.NewBlock(),
|
||||
inputStream: lines,
|
||||
render: make(chan bool),
|
||||
Text: []ToggleText{},
|
||||
TextOut: []string{},
|
||||
TextFgColor: ui.ThemeAttr("menu.text.fg"),
|
||||
TextBgColor: ui.ThemeAttr("menu.text.bg"),
|
||||
padding: Padding{4, 2},
|
||||
}
|
||||
|
||||
t.BorderFg = ui.ThemeAttr("menu.border.fg")
|
||||
t.BorderLabelFg = ui.ThemeAttr("menu.label.fg")
|
||||
t.Height = ui.TermHeight()
|
||||
t.Width = ui.TermWidth()
|
||||
|
||||
t.readInputLoop()
|
||||
t.renderLoop()
|
||||
return t
|
||||
}
|
||||
|
||||
// Adjusts text inside this view according to the window size. No need to call ui.Render(...)
|
||||
// after calling this method, it is called automatically
|
||||
func (t *TextView) Resize() {
|
||||
ui.Clear()
|
||||
t.Height = ui.TermHeight()
|
||||
t.Width = ui.TermWidth()
|
||||
t.render <- true
|
||||
}
|
||||
|
||||
// Toggles text inside this view. No need to call ui.Render(...) after calling this method,
|
||||
// it is called automatically
|
||||
func (t *TextView) Toggle() {
|
||||
t.toggleState = !t.toggleState
|
||||
t.render <- true
|
||||
}
|
||||
|
||||
func (t *TextView) Buffer() ui.Buffer {
|
||||
var cell ui.Cell
|
||||
buf := t.Block.Buffer()
|
||||
|
||||
x := t.Block.X + t.padding[0]
|
||||
y := t.Block.Y + t.padding[1]
|
||||
|
||||
for _, line := range t.TextOut {
|
||||
for _, ch := range line {
|
||||
cell = ui.Cell{Ch: ch, Fg: t.TextFgColor, Bg: t.TextBgColor}
|
||||
buf.Set(x, y, cell)
|
||||
x++
|
||||
}
|
||||
x = t.Block.X + t.padding[0]
|
||||
y++
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
func (t *TextView) renderLoop() {
|
||||
go func() {
|
||||
for range t.render {
|
||||
maxWidth := t.Width - (t.padding[0] * 2)
|
||||
height := t.Height - (t.padding[1] * 2)
|
||||
t.TextOut = []string{}
|
||||
for i := len(t.Text) - 1; i >= 0; i-- {
|
||||
lines := splitLine(t.Text[i].Toggle(t.toggleState), maxWidth)
|
||||
t.TextOut = append(lines, t.TextOut...)
|
||||
if len(t.TextOut) > height {
|
||||
t.TextOut = t.TextOut[:height]
|
||||
break
|
||||
}
|
||||
}
|
||||
ui.Render(t)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (t *TextView) readInputLoop() {
|
||||
go func() {
|
||||
for line := range t.inputStream {
|
||||
t.Text = append(t.Text, line)
|
||||
t.render <- true
|
||||
}
|
||||
close(t.render)
|
||||
}()
|
||||
}
|
||||
|
||||
func splitLine(line string, lineSize int) []string {
|
||||
if line == "" {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
var lines []string
|
||||
for {
|
||||
if len(line) <= lineSize {
|
||||
lines = append(lines, line)
|
||||
return lines
|
||||
}
|
||||
lines = append(lines, line[:lineSize])
|
||||
line = line[lineSize:]
|
||||
}
|
||||
}
|
||||
35
widgets/view_test.go
Normal file
35
widgets/view_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package widgets
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSplitEmptyLine(t *testing.T) {
|
||||
|
||||
result := splitLine("", 5)
|
||||
if len(result) != 0 {
|
||||
t.Errorf("expected: 0 lines, got: %d", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitLineShorterThanLimit(t *testing.T) {
|
||||
|
||||
result := splitLine("hello", 7)
|
||||
if len(result) != 1 {
|
||||
t.Errorf("expected: 0 lines, got: %d", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitLineLongerThanLimit(t *testing.T) {
|
||||
|
||||
result := splitLine("hello", 3)
|
||||
if len(result) != 2 {
|
||||
t.Errorf("expected: 0 lines, got: %d", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitLineSameAsLimit(t *testing.T) {
|
||||
|
||||
result := splitLine("hello", 5)
|
||||
if len(result) != 1 {
|
||||
t.Errorf("expected: 0 lines, got: %d", len(result))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user