mirror of
https://github.com/bcicen/ctop.git
synced 2025-12-06 23:26:45 +08:00
Compare commits
201 Commits
v0.6.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 |
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
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
ctop
|
ctop
|
||||||
.idea
|
.idea
|
||||||
|
/vendor/
|
||||||
11
Dockerfile
11
Dockerfile
@@ -1,16 +1,17 @@
|
|||||||
FROM quay.io/vektorcloud/go:1.8
|
FROM quay.io/vektorcloud/go:1.13
|
||||||
|
|
||||||
RUN apk add --no-cache make
|
RUN apk add --no-cache make
|
||||||
|
|
||||||
COPY glide.* /go/src/github.com/bcicen/ctop/
|
WORKDIR /app
|
||||||
WORKDIR /go/src/github.com/bcicen/ctop/
|
COPY go.mod .
|
||||||
RUN glide install
|
RUN go mod download
|
||||||
|
|
||||||
COPY . /go/src/github.com/bcicen/ctop
|
COPY . .
|
||||||
RUN make build && \
|
RUN make build && \
|
||||||
mkdir -p /go/bin && \
|
mkdir -p /go/bin && \
|
||||||
mv -v ctop /go/bin/
|
mv -v ctop /go/bin/
|
||||||
|
|
||||||
FROM scratch
|
FROM scratch
|
||||||
|
ENV TERM=linux
|
||||||
COPY --from=0 /go/bin/ctop /ctop
|
COPY --from=0 /go/bin/ctop /ctop
|
||||||
ENTRYPOINT ["/ctop"]
|
ENTRYPOINT ["/ctop"]
|
||||||
|
|||||||
32
Makefile
32
Makefile
@@ -1,33 +1,37 @@
|
|||||||
NAME=ctop
|
NAME=ctop
|
||||||
VERSION=$(shell cat VERSION)
|
VERSION=$(shell cat VERSION)
|
||||||
BUILD=$(shell git rev-parse --short HEAD)
|
BUILD=$(shell git rev-parse --short HEAD)
|
||||||
EXT_LD_FLAGS="-Wl,--allow-multiple-definition"
|
LD_FLAGS="-w -X main.version=$(VERSION) -X main.build=$(BUILD)"
|
||||||
LD_FLAGS="-w -X main.version=$(VERSION) -X main.build=$(BUILD) -extldflags=$(EXT_LD_FLAGS)"
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf _build/ _release/
|
rm -rf _build/ release/
|
||||||
|
|
||||||
build:
|
build:
|
||||||
glide install
|
go mod download
|
||||||
CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o ctop
|
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) -extldflags=$(EXT_LD_FLAGS)"
|
|
||||||
|
|
||||||
build-all:
|
build-all:
|
||||||
mkdir -p build
|
mkdir -p _build
|
||||||
GOOS=darwin GOARCH=amd64 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-darwin-amd64
|
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 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=amd64 CGO_ENABLED=0 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
|
GOOS=linux GOARCH=arm CGO_ENABLED=0 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-arm
|
||||||
GOOS=linux GOARCH=arm64 go build -tags release -ldflags $(LD_FLAGS) -o _build/ctop-$(VERSION)-linux-arm64
|
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:
|
image:
|
||||||
docker build -t ctop -f Dockerfile .
|
docker build -t ctop -f Dockerfile .
|
||||||
|
|
||||||
release:
|
release:
|
||||||
mkdir _release
|
mkdir release
|
||||||
go get github.com/progrium/gh-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) \
|
gh-release create bcicen/$(NAME) $(VERSION) \
|
||||||
$(shell git rev-parse --abbrev-ref HEAD) $(VERSION)
|
$(shell git rev-parse --abbrev-ref HEAD) $(VERSION)
|
||||||
|
|
||||||
|
|||||||
56
README.md
56
README.md
@@ -9,7 +9,7 @@ Top-like interface for container metrics
|
|||||||
`ctop` provides a concise and condensed overview of real-time metrics for multiple containers:
|
`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>
|
<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` comes with built-in support for Docker and runC; 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.
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ Fetch the [latest release](https://github.com/bcicen/ctop/releases) for your pla
|
|||||||
#### Linux
|
#### Linux
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo wget https://github.com/bcicen/ctop/releases/download/v0.6.0/ctop-0.6.0-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
|
sudo chmod +x /usr/local/bin/ctop
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ brew install ctop
|
|||||||
```
|
```
|
||||||
or
|
or
|
||||||
```bash
|
```bash
|
||||||
sudo curl -Lo /usr/local/bin/ctop https://github.com/bcicen/ctop/releases/download/v0.6.0/ctop-0.6.0-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
|
sudo chmod +x /usr/local/bin/ctop
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ sudo chmod +x /usr/local/bin/ctop
|
|||||||
```bash
|
```bash
|
||||||
docker run --rm -ti \
|
docker run --rm -ti \
|
||||||
--name=ctop \
|
--name=ctop \
|
||||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
--volume /var/run/docker.sock:/var/run/docker.sock:ro \
|
||||||
quay.io/vektorlab/ctop:latest
|
quay.io/vektorlab/ctop:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -54,32 +54,46 @@ Build steps can be found [here][build].
|
|||||||
|
|
||||||
`ctop` requires no arguments and uses Docker host variables by default. See [connectors][connectors] for further configuration options.
|
`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
|
### Options
|
||||||
|
|
||||||
Option | Description
|
Option | Description
|
||||||
--- | ---
|
--- | ---
|
||||||
-a | show active containers only
|
`-a` | show active containers only
|
||||||
-f <string> | set an initial filter string
|
`-f <string>` | set an initial filter string
|
||||||
-h | display help dialog
|
`-h` | display help dialog
|
||||||
-i | invert default colors
|
`-i` | invert default colors
|
||||||
-r | reverse container sort order
|
`-r` | reverse container sort order
|
||||||
-s | select initial container sort field
|
`-s` | select initial container sort field
|
||||||
-v | output version information and exit
|
`-scale-cpu` | show cpu as % of system total
|
||||||
|
`-v` | output version information and exit
|
||||||
|
`-shell` | exec shell to use (default: sh)
|
||||||
|
|
||||||
### Keybindings
|
### Keybindings
|
||||||
|
|
||||||
Key | Action
|
| Key | Action |
|
||||||
--- | ---
|
| :----------------------: | ---------------------------------------------------------- |
|
||||||
a | Toggle display of all (running and non-running) containers
|
| <kbd><ENTER></kbd> | Open container menu |
|
||||||
f | Filter displayed containers (`esc` to clear when open)
|
| <kbd>a</kbd> | Toggle display of all (running and non-running) containers |
|
||||||
H | Toggle ctop header
|
| <kbd>f</kbd> | Filter displayed containers (`esc` to clear when open) |
|
||||||
h | Open help dialog
|
| <kbd>H</kbd> | Toggle ctop header |
|
||||||
s | Select container sort field
|
| <kbd>h</kbd> | Open help dialog |
|
||||||
r | Reverse container sort order
|
| <kbd>s</kbd> | Select container sort field |
|
||||||
q | Quit ctop
|
| <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
|
[build]: _docs/build.md
|
||||||
[connectors]: _docs/connectors.md
|
[connectors]: _docs/connectors.md
|
||||||
[expanded_view]: _docs/expanded.md
|
[single_view]: _docs/single.md
|
||||||
[release]: https://img.shields.io/github/release/bcicen/ctop.svg "ctop"
|
[release]: https://img.shields.io/github/release/bcicen/ctop.svg "ctop"
|
||||||
[homebrew]: https://img.shields.io/homebrew/v/ctop.svg "ctop"
|
[homebrew]: https://img.shields.io/homebrew/v/ctop.svg "ctop"
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
# Build
|
# 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
|
```bash
|
||||||
go get github.com/bcicen/ctop && \
|
|
||||||
cd $GOPATH/src/github.com/bcicen/ctop && \
|
|
||||||
make build
|
make build
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -16,5 +14,8 @@ make image
|
|||||||
Now you can run your local image:
|
Now you can run your local image:
|
||||||
|
|
||||||
```bash
|
```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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# connectors
|
# Connectors
|
||||||
|
|
||||||
`ctop` comes with the below native connectors, enabled via the `--connector` option.
|
`ctop` comes with the below native connectors, enabled via the `--connector` option.
|
||||||
|
|
||||||
@@ -16,11 +16,11 @@ DOCKER_HOST | Daemon socket to connect to (default: `unix://var/run/docker.sock`
|
|||||||
|
|
||||||
## RunC
|
## RunC
|
||||||
|
|
||||||
Using this connector requires full privileges to the local runC root dir (default: `/run/runc`)
|
Using this connector requires full privileges to the local runC root dir of container state (default: `/run/runc`)
|
||||||
|
|
||||||
#### Options
|
#### Options
|
||||||
|
|
||||||
Var | Description
|
Var | Description
|
||||||
--- | ---
|
--- | ---
|
||||||
RUNC_ROOT | path to runc root (default: `/run/runc`)
|
RUNC_ROOT | path to runc root for container state (default: `/run/runc`)
|
||||||
RUNC_SYSTEMD_CGROUP | if set, enable systemd cgroups
|
RUNC_SYSTEMD_CGROUP | if set, enable systemd cgroups
|
||||||
|
|||||||
@@ -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,
|
"par.text.hi": ui.ColorBlack,
|
||||||
"sparkline.line.fg": ui.ColorGreen,
|
"sparkline.line.fg": ui.ColorGreen,
|
||||||
"sparkline.title.fg": ui.ColorWhite,
|
"sparkline.title.fg": ui.ColorWhite,
|
||||||
|
"status.ok": ui.ColorGreen,
|
||||||
|
"status.warn": ui.ColorYellow,
|
||||||
|
"status.danger": ui.ColorRed,
|
||||||
}
|
}
|
||||||
|
|
||||||
func InvertColorMap() {
|
func InvertColorMap() {
|
||||||
re := regexp.MustCompile(".*.fg")
|
re := regexp.MustCompile(".*.fg")
|
||||||
for k, _ := range ColorMap {
|
for k := range ColorMap {
|
||||||
if re.FindAllString(k, 1) != nil {
|
if re.FindAllString(k, 1) != nil {
|
||||||
ColorMap[k] = ui.ColorBlack
|
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 (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/bcicen/ctop/logging"
|
"github.com/bcicen/ctop/logging"
|
||||||
)
|
)
|
||||||
@@ -10,17 +11,24 @@ import (
|
|||||||
var (
|
var (
|
||||||
GlobalParams []*Param
|
GlobalParams []*Param
|
||||||
GlobalSwitches []*Switch
|
GlobalSwitches []*Switch
|
||||||
|
GlobalColumns []*Column
|
||||||
|
lock sync.RWMutex
|
||||||
log = logging.Init()
|
log = logging.Init()
|
||||||
)
|
)
|
||||||
|
|
||||||
func Init() {
|
func Init() {
|
||||||
for _, p := range params {
|
for _, p := range defaultParams {
|
||||||
GlobalParams = append(GlobalParams, p)
|
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)
|
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
|
package config
|
||||||
|
|
||||||
// defaults
|
// defaults
|
||||||
var params = []*Param{
|
var defaultParams = []*Param{
|
||||||
&Param{
|
&Param{
|
||||||
Key: "filterStr",
|
Key: "filterStr",
|
||||||
Val: "",
|
Val: "",
|
||||||
@@ -12,6 +12,16 @@ var params = []*Param{
|
|||||||
Val: "state",
|
Val: "state",
|
||||||
Label: "Container Sort Field",
|
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 {
|
type Param struct {
|
||||||
@@ -22,6 +32,9 @@ type Param struct {
|
|||||||
|
|
||||||
// Get Param by key
|
// Get Param by key
|
||||||
func Get(k string) *Param {
|
func Get(k string) *Param {
|
||||||
|
lock.RLock()
|
||||||
|
defer lock.RUnlock()
|
||||||
|
|
||||||
for _, p := range GlobalParams {
|
for _, p := range GlobalParams {
|
||||||
if p.Key == k {
|
if p.Key == k {
|
||||||
return p
|
return p
|
||||||
@@ -30,7 +43,7 @@ func Get(k string) *Param {
|
|||||||
return &Param{} // default
|
return &Param{} // default
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get Param value by key
|
// GetVal gets Param value by key
|
||||||
func GetVal(k string) string {
|
func GetVal(k string) string {
|
||||||
return Get(k).Val
|
return Get(k).Val
|
||||||
}
|
}
|
||||||
@@ -38,7 +51,10 @@ func GetVal(k string) string {
|
|||||||
// Set param value
|
// Set param value
|
||||||
func Update(k, v string) {
|
func Update(k, v string) {
|
||||||
p := Get(k)
|
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
|
p.Val = v
|
||||||
// log.Errorf("ignoring update for non-existant parameter: %s", k)
|
// log.Errorf("ignoring update for non-existant parameter: %s", k)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,31 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
// defaults
|
// defaults
|
||||||
var switches = []*Switch{
|
var defaultSwitches = []*Switch{
|
||||||
&Switch{
|
&Switch{
|
||||||
Key: "sortReversed",
|
Key: "sortReversed",
|
||||||
Val: false,
|
Val: false,
|
||||||
Label: "Reverse Sort Order",
|
Label: "Reverse sort order",
|
||||||
},
|
},
|
||||||
&Switch{
|
&Switch{
|
||||||
Key: "allContainers",
|
Key: "allContainers",
|
||||||
Val: true,
|
Val: true,
|
||||||
Label: "Show All Containers",
|
Label: "Show all containers",
|
||||||
|
},
|
||||||
|
&Switch{
|
||||||
|
Key: "fullRowCursor",
|
||||||
|
Val: true,
|
||||||
|
Label: "Highlight entire cursor row (vs. name only)",
|
||||||
},
|
},
|
||||||
&Switch{
|
&Switch{
|
||||||
Key: "enableHeader",
|
Key: "enableHeader",
|
||||||
Val: true,
|
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
|
Label string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return Switch by key
|
// GetSwitch returns Switch by key
|
||||||
func GetSwitch(k string) *Switch {
|
func GetSwitch(k string) *Switch {
|
||||||
|
lock.RLock()
|
||||||
|
defer lock.RUnlock()
|
||||||
|
|
||||||
for _, sw := range GlobalSwitches {
|
for _, sw := range GlobalSwitches {
|
||||||
if sw.Key == k {
|
if sw.Key == k {
|
||||||
return sw
|
return sw
|
||||||
@@ -35,16 +48,31 @@ func GetSwitch(k string) *Switch {
|
|||||||
return &Switch{} // default
|
return &Switch{} // default
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return Switch value by key
|
// GetSwitchVal returns Switch value by key
|
||||||
func GetSwitchVal(k string) bool {
|
func GetSwitchVal(k string) bool {
|
||||||
return GetSwitch(k).Val
|
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
|
// Toggle a boolean switch
|
||||||
func Toggle(k string) {
|
func Toggle(k string) {
|
||||||
sw := GetSwitch(k)
|
sw := GetSwitch(k)
|
||||||
newVal := sw.Val != true
|
|
||||||
log.Noticef("config change: %s: %t -> %t", k, sw.Val, newVal)
|
lock.Lock()
|
||||||
sw.Val = newVal
|
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)
|
//log.Errorf("ignoring toggle for non-existant switch: %s", k)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package collector
|
package collector
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/bcicen/ctop/config"
|
||||||
"github.com/bcicen/ctop/models"
|
"github.com/bcicen/ctop/models"
|
||||||
api "github.com/fsouza/go-dockerclient"
|
api "github.com/fsouza/go-dockerclient"
|
||||||
)
|
)
|
||||||
@@ -15,6 +16,7 @@ type Docker struct {
|
|||||||
done chan bool
|
done chan bool
|
||||||
lastCpu float64
|
lastCpu float64
|
||||||
lastSysCpu float64
|
lastSysCpu float64
|
||||||
|
scaleCpu bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDocker(client *api.Client, id string) *Docker {
|
func NewDocker(client *api.Client, id string) *Docker {
|
||||||
@@ -22,6 +24,7 @@ func NewDocker(client *api.Client, id string) *Docker {
|
|||||||
Metrics: models.Metrics{},
|
Metrics: models.Metrics{},
|
||||||
id: id,
|
id: id,
|
||||||
client: client,
|
client: client,
|
||||||
|
scaleCpu: config.GetSwitchVal("scaleCpu"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +69,7 @@ func (c *Docker) Stream() chan models.Metrics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Docker) Logs() LogCollector {
|
func (c *Docker) Logs() LogCollector {
|
||||||
return &DockerLogs{c.id, c.client, make(chan bool)}
|
return NewDockerLogs(c.id, c.client)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop collector
|
// Stop collector
|
||||||
@@ -82,14 +85,18 @@ func (c *Docker) ReadCPU(stats *api.Stats) {
|
|||||||
cpudiff := total - c.lastCpu
|
cpudiff := total - c.lastCpu
|
||||||
syscpudiff := system - c.lastSysCpu
|
syscpudiff := system - c.lastSysCpu
|
||||||
|
|
||||||
|
if c.scaleCpu {
|
||||||
|
c.CPUUtil = round((cpudiff / syscpudiff * 100))
|
||||||
|
} else {
|
||||||
c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
|
c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
|
||||||
|
}
|
||||||
c.lastCpu = total
|
c.lastCpu = total
|
||||||
c.lastSysCpu = system
|
c.lastSysCpu = system
|
||||||
c.Pids = int(stats.PidsStats.Current)
|
c.Pids = int(stats.PidsStats.Current)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Docker) ReadMem(stats *api.Stats) {
|
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.MemLimit = int64(stats.MemoryStats.Limit)
|
||||||
c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
|
c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
|
||||||
}
|
}
|
||||||
@@ -107,10 +114,10 @@ func (c *Docker) ReadIO(stats *api.Stats) {
|
|||||||
var read, write int64
|
var read, write int64
|
||||||
for _, blk := range stats.BlkioStats.IOServiceBytesRecursive {
|
for _, blk := range stats.BlkioStats.IOServiceBytesRecursive {
|
||||||
if blk.Op == "Read" {
|
if blk.Op == "Read" {
|
||||||
read = int64(blk.Value)
|
read += int64(blk.Value)
|
||||||
}
|
}
|
||||||
if blk.Op == "Write" {
|
if blk.Op == "Write" {
|
||||||
write = int64(blk.Value)
|
write += int64(blk.Value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.IOBytesRead, c.IOBytesWrite = read, write
|
c.IOBytesRead, c.IOBytesWrite = read, write
|
||||||
|
|||||||
@@ -17,6 +17,14 @@ type DockerLogs struct {
|
|||||||
done chan bool
|
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 {
|
func (l *DockerLogs) Stream() chan models.Log {
|
||||||
r, w := io.Pipe()
|
r, w := io.Pipe()
|
||||||
logCh := make(chan models.Log)
|
logCh := make(chan models.Log)
|
||||||
@@ -26,12 +34,13 @@ func (l *DockerLogs) Stream() chan models.Log {
|
|||||||
Context: ctx,
|
Context: ctx,
|
||||||
Container: l.id,
|
Container: l.id,
|
||||||
OutputStream: w,
|
OutputStream: w,
|
||||||
ErrorStream: w,
|
//ErrorStream: w,
|
||||||
Stdout: true,
|
Stdout: true,
|
||||||
Stderr: true,
|
Stderr: true,
|
||||||
Tail: "10",
|
Tail: "20",
|
||||||
Follow: true,
|
Follow: true,
|
||||||
Timestamps: true,
|
Timestamps: true,
|
||||||
|
RawTerminal: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// read io pipe into channel
|
// read io pipe into channel
|
||||||
@@ -40,7 +49,7 @@ func (l *DockerLogs) Stream() chan models.Log {
|
|||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
parts := strings.Split(scanner.Text(), " ")
|
parts := strings.Split(scanner.Text(), " ")
|
||||||
ts := l.parseTime(parts[0])
|
ts := l.parseTime(parts[0])
|
||||||
logCh <- models.Log{ts, strings.Join(parts[1:], " ")}
|
logCh <- models.Log{Timestamp: ts, Message: strings.Join(parts[1:], " ")}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -50,15 +59,15 @@ func (l *DockerLogs) Stream() chan models.Log {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("error reading container logs: %s", err)
|
log.Errorf("error reading container logs: %s", err)
|
||||||
}
|
}
|
||||||
|
log.Infof("log reader stopped for container: %s", l.id)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
select {
|
<-l.done
|
||||||
case <-l.done:
|
|
||||||
cancel()
|
cancel()
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
log.Infof("log reader started for container: %s", l.id)
|
||||||
return logCh
|
return logCh
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,9 +75,25 @@ func (l *DockerLogs) Stop() { l.done <- true }
|
|||||||
|
|
||||||
func (l *DockerLogs) parseTime(s string) time.Time {
|
func (l *DockerLogs) parseTime(s string) time.Time {
|
||||||
ts, err := time.Parse("2006-01-02T15:04:05.000000000Z", s)
|
ts, err := time.Parse("2006-01-02T15:04:05.000000000Z", s)
|
||||||
if err != nil {
|
if err == nil {
|
||||||
log.Errorf("failed to parse container log: %s", err)
|
|
||||||
ts = time.Now()
|
|
||||||
}
|
|
||||||
return ts
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func (l *MockLogs) Stream() chan models.Log {
|
|||||||
case <-l.done:
|
case <-l.done:
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
logCh <- models.Log{time.Now(), mockLog}
|
logCh <- models.Log{Timestamp: time.Now(), Message: mockLog}
|
||||||
time.Sleep(250 * time.Millisecond)
|
time.Sleep(250 * time.Millisecond)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
// +build !darwin
|
// +build linux
|
||||||
|
|
||||||
package collector
|
package collector
|
||||||
|
|
||||||
import (
|
import (
|
||||||
linuxproc "github.com/c9s/goprocinfo/linux"
|
linuxproc "github.com/c9s/goprocinfo/linux"
|
||||||
"github.com/opencontainers/runc/libcontainer/system"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var sysMemTotal = getSysMemTotal()
|
var sysMemTotal = getSysMemTotal()
|
||||||
var clockTicksPerSecond = uint64(system.GetClockTicks())
|
|
||||||
|
|
||||||
const nanoSecondsPerSecond = 1e9
|
const (
|
||||||
|
clockTicksPerSecond uint64 = 100
|
||||||
|
nanoSecondsPerSecond = 1e9
|
||||||
|
)
|
||||||
|
|
||||||
func getSysMemTotal() int64 {
|
func getSysMemTotal() int64 {
|
||||||
stat, err := linuxproc.ReadMemInfo("/proc/meminfo")
|
stat, err := linuxproc.ReadMemInfo("/proc/meminfo")
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
// +build !darwin
|
// +build linux
|
||||||
|
|
||||||
package collector
|
package collector
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bcicen/ctop/models"
|
|
||||||
"github.com/opencontainers/runc/libcontainer"
|
"github.com/opencontainers/runc/libcontainer"
|
||||||
"github.com/opencontainers/runc/libcontainer/cgroups"
|
"github.com/opencontainers/runc/libcontainer/cgroups"
|
||||||
|
"github.com/opencontainers/runc/types"
|
||||||
|
|
||||||
|
"github.com/bcicen/ctop/config"
|
||||||
|
"github.com/bcicen/ctop/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Runc collector
|
// Runc collector
|
||||||
@@ -21,6 +24,7 @@ type Runc struct {
|
|||||||
interval int // collection interval, in seconds
|
interval int // collection interval, in seconds
|
||||||
lastCpu float64
|
lastCpu float64
|
||||||
lastSysCpu float64
|
lastSysCpu float64
|
||||||
|
scaleCpu bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRunc(libc libcontainer.Container) *Runc {
|
func NewRunc(libc libcontainer.Container) *Runc {
|
||||||
@@ -29,6 +33,7 @@ func NewRunc(libc libcontainer.Container) *Runc {
|
|||||||
id: libc.ID(),
|
id: libc.ID(),
|
||||||
libc: libc,
|
libc: libc,
|
||||||
interval: 1,
|
interval: 1,
|
||||||
|
scaleCpu: config.GetSwitchVal("scaleCpu"),
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
@@ -90,7 +95,11 @@ func (c *Runc) ReadCPU(stats *cgroups.Stats) {
|
|||||||
cpudiff := total - c.lastCpu
|
cpudiff := total - c.lastCpu
|
||||||
syscpudiff := system - c.lastSysCpu
|
syscpudiff := system - c.lastSysCpu
|
||||||
|
|
||||||
|
if c.scaleCpu {
|
||||||
|
c.CPUUtil = round((cpudiff / syscpudiff * 100))
|
||||||
|
} else {
|
||||||
c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
|
c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus)
|
||||||
|
}
|
||||||
c.lastCpu = total
|
c.lastCpu = total
|
||||||
c.lastSysCpu = system
|
c.lastSysCpu = system
|
||||||
c.Pids = int(stats.PidsStats.Current)
|
c.Pids = int(stats.PidsStats.Current)
|
||||||
@@ -105,7 +114,7 @@ func (c *Runc) ReadMem(stats *cgroups.Stats) {
|
|||||||
c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
|
c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Runc) ReadNet(interfaces []*libcontainer.NetworkInterface) {
|
func (c *Runc) ReadNet(interfaces []*types.NetworkInterface) {
|
||||||
var rx, tx int64
|
var rx, tx int64
|
||||||
for _, network := range interfaces {
|
for _, network := range interfaces {
|
||||||
rx += int64(network.RxBytes)
|
rx += int64(network.RxBytes)
|
||||||
|
|||||||
@@ -6,35 +6,56 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/bcicen/ctop/connector/collector"
|
"github.com/bcicen/ctop/connector/collector"
|
||||||
|
"github.com/bcicen/ctop/connector/manager"
|
||||||
"github.com/bcicen/ctop/container"
|
"github.com/bcicen/ctop/container"
|
||||||
api "github.com/fsouza/go-dockerclient"
|
api "github.com/fsouza/go-dockerclient"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() { enabled["docker"] = NewDocker }
|
||||||
|
|
||||||
type Docker struct {
|
type Docker struct {
|
||||||
client *api.Client
|
client *api.Client
|
||||||
containers map[string]*container.Container
|
containers map[string]*container.Container
|
||||||
needsRefresh chan string // container IDs requiring refresh
|
needsRefresh chan string // container IDs requiring refresh
|
||||||
|
closed chan struct{}
|
||||||
lock sync.RWMutex
|
lock sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDocker() Connector {
|
func NewDocker() (Connector, error) {
|
||||||
// init docker client
|
// init docker client
|
||||||
client, err := api.NewClientFromEnv()
|
client, err := api.NewClientFromEnv()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
cm := &Docker{
|
cm := &Docker{
|
||||||
client: client,
|
client: client,
|
||||||
containers: make(map[string]*container.Container),
|
containers: make(map[string]*container.Container),
|
||||||
needsRefresh: make(chan string, 60),
|
needsRefresh: make(chan string, 60),
|
||||||
|
closed: make(chan struct{}),
|
||||||
lock: sync.RWMutex{},
|
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()
|
go cm.Loop()
|
||||||
cm.refreshAll()
|
cm.refreshAll()
|
||||||
go cm.watchEvents()
|
go cm.watchEvents()
|
||||||
return cm
|
return cm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Docker implements Connector
|
||||||
|
func (cm *Docker) Wait() struct{} { return <-cm.closed }
|
||||||
|
|
||||||
// Docker events watcher
|
// Docker events watcher
|
||||||
func (cm *Docker) watchEvents() {
|
func (cm *Docker) watchEvents() {
|
||||||
log.Info("docker event listener starting")
|
log.Info("docker event listener starting")
|
||||||
@@ -45,8 +66,11 @@ func (cm *Docker) watchEvents() {
|
|||||||
if e.Type != "container" {
|
if e.Type != "container" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch e.Action {
|
|
||||||
case "start", "die", "pause", "unpause":
|
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)
|
log.Debugf("handling docker event: action=%s id=%s", e.Action, e.ID)
|
||||||
cm.needsRefresh <- e.ID
|
cm.needsRefresh <- e.ID
|
||||||
case "destroy":
|
case "destroy":
|
||||||
@@ -54,6 +78,8 @@ func (cm *Docker) watchEvents() {
|
|||||||
cm.delByID(e.ID)
|
cm.delByID(e.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
log.Info("docker event listener exited")
|
||||||
|
close(cm.closed)
|
||||||
}
|
}
|
||||||
|
|
||||||
func portsFormat(ports map[api.Port][]api.PortBinding) string {
|
func portsFormat(ports map[api.Port][]api.PortBinding) string {
|
||||||
@@ -74,6 +100,17 @@ func portsFormat(ports map[api.Port][]api.PortBinding) string {
|
|||||||
return strings.Join(append(exposed, published...), "\n")
|
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) {
|
func (cm *Docker) refresh(c *container.Container) {
|
||||||
insp := cm.inspect(c.Id)
|
insp := cm.inspect(c.Id)
|
||||||
// remove container if no longer exists
|
// remove container if no longer exists
|
||||||
@@ -83,16 +120,21 @@ func (cm *Docker) refresh(c *container.Container) {
|
|||||||
}
|
}
|
||||||
c.SetMeta("name", shortName(insp.Name))
|
c.SetMeta("name", shortName(insp.Name))
|
||||||
c.SetMeta("image", insp.Config.Image)
|
c.SetMeta("image", insp.Config.Image)
|
||||||
|
c.SetMeta("IPs", ipsFormat(insp.NetworkSettings.Networks))
|
||||||
c.SetMeta("ports", portsFormat(insp.NetworkSettings.Ports))
|
c.SetMeta("ports", portsFormat(insp.NetworkSettings.Ports))
|
||||||
c.SetMeta("created", insp.Created.Format("Mon Jan 2 15:04:05 2006"))
|
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)
|
c.SetState(insp.State.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cm *Docker) inspect(id string) *api.Container {
|
func (cm *Docker) inspect(id string) *api.Container {
|
||||||
c, err := cm.client.InspectContainer(id)
|
c, err := cm.client.InspectContainer(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(*api.NoSuchContainer); ok == false {
|
if _, ok := err.(*api.NoSuchContainer); !ok {
|
||||||
log.Errorf(err.Error())
|
log.Errorf("%s (%T)", err.Error(), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
@@ -103,7 +145,8 @@ func (cm *Docker) refreshAll() {
|
|||||||
opts := api.ListContainersOptions{All: true}
|
opts := api.ListContainersOptions{All: true}
|
||||||
allContainers, err := cm.client.ListContainers(opts)
|
allContainers, err := cm.client.ListContainers(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
log.Errorf("%s (%T)", err.Error(), err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, i := range allContainers {
|
for _, i := range allContainers {
|
||||||
@@ -115,21 +158,28 @@ func (cm *Docker) refreshAll() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (cm *Docker) Loop() {
|
func (cm *Docker) Loop() {
|
||||||
for id := range cm.needsRefresh {
|
for {
|
||||||
|
select {
|
||||||
|
case id := <-cm.needsRefresh:
|
||||||
c := cm.MustGet(id)
|
c := cm.MustGet(id)
|
||||||
cm.refresh(c)
|
cm.refresh(c)
|
||||||
|
case <-cm.closed:
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a single container, creating one anew if not existing
|
// MustGet gets a single container, creating one anew if not existing
|
||||||
func (cm *Docker) MustGet(id string) *container.Container {
|
func (cm *Docker) MustGet(id string) *container.Container {
|
||||||
c, ok := cm.Get(id)
|
c, ok := cm.Get(id)
|
||||||
// append container struct for new containers
|
// append container struct for new containers
|
||||||
if !ok {
|
if !ok {
|
||||||
// create collector
|
// create collector
|
||||||
collector := collector.NewDocker(cm.client, id)
|
collector := collector.NewDocker(cm.client, id)
|
||||||
|
// create manager
|
||||||
|
manager := manager.NewDocker(cm.client, id)
|
||||||
// create container
|
// create container
|
||||||
c = container.New(id, collector)
|
c = container.New(id, collector, manager)
|
||||||
cm.lock.Lock()
|
cm.lock.Lock()
|
||||||
cm.containers[id] = c
|
cm.containers[id] = c
|
||||||
cm.lock.Unlock()
|
cm.lock.Unlock()
|
||||||
@@ -137,7 +187,7 @@ func (cm *Docker) MustGet(id string) *container.Container {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a single container, by ID
|
// Docker implements Connector
|
||||||
func (cm *Docker) Get(id string) (*container.Container, bool) {
|
func (cm *Docker) Get(id string) (*container.Container, bool) {
|
||||||
cm.lock.Lock()
|
cm.lock.Lock()
|
||||||
c, ok := cm.containers[id]
|
c, ok := cm.containers[id]
|
||||||
@@ -153,12 +203,13 @@ func (cm *Docker) delByID(id string) {
|
|||||||
log.Infof("removed dead container: %s", id)
|
log.Infof("removed dead container: %s", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return array of all containers, sorted by field
|
// Docker implements Connector
|
||||||
func (cm *Docker) All() (containers container.Containers) {
|
func (cm *Docker) All() (containers container.Containers) {
|
||||||
cm.lock.Lock()
|
cm.lock.Lock()
|
||||||
for _, c := range cm.containers {
|
for _, c := range cm.containers {
|
||||||
containers = append(containers, c)
|
containers = append(containers, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
containers.Sort()
|
containers.Sort()
|
||||||
containers.Filter()
|
containers.Filter()
|
||||||
cm.lock.Unlock()
|
cm.lock.Unlock()
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
// +build !linux
|
|
||||||
|
|
||||||
package connector
|
|
||||||
|
|
||||||
var enabled = map[string]func() Connector{
|
|
||||||
"docker": NewDocker,
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
// +build !darwin
|
|
||||||
|
|
||||||
package connector
|
|
||||||
|
|
||||||
var enabled = map[string]func() Connector{
|
|
||||||
"docker": NewDocker,
|
|
||||||
"runc": NewRunc,
|
|
||||||
}
|
|
||||||
@@ -2,25 +2,103 @@ package connector
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/bcicen/ctop/container"
|
"github.com/bcicen/ctop/container"
|
||||||
"github.com/bcicen/ctop/logging"
|
"github.com/bcicen/ctop/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
var log = logging.Init()
|
var (
|
||||||
|
log = logging.Init()
|
||||||
|
enabled = make(map[string]ConnectorFn)
|
||||||
|
)
|
||||||
|
|
||||||
func ByName(s string) (Connector, error) {
|
type ConnectorFn func() (Connector, error)
|
||||||
if _, ok := enabled[s]; !ok {
|
|
||||||
msg := fmt.Sprintf("invalid connector type \"%s\"\nconnector must be one of:", s)
|
|
||||||
for k, _ := range enabled {
|
|
||||||
msg += fmt.Sprintf("\n %s", k)
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf(msg)
|
|
||||||
}
|
|
||||||
return enabled[s](), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type Connector interface {
|
type Connector interface {
|
||||||
|
// All returns a pre-sorted container.Containers of all discovered containers
|
||||||
All() container.Containers
|
All() container.Containers
|
||||||
|
// Get returns a single container.Container by ID
|
||||||
Get(string) (*container.Container, bool)
|
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
|
||||||
|
}
|
||||||
@@ -8,20 +8,23 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bcicen/ctop/connector/collector"
|
"github.com/bcicen/ctop/connector/collector"
|
||||||
|
"github.com/bcicen/ctop/connector/manager"
|
||||||
"github.com/bcicen/ctop/container"
|
"github.com/bcicen/ctop/container"
|
||||||
"github.com/jgautheron/codename-generator"
|
"github.com/jgautheron/codename-generator"
|
||||||
"github.com/nu7hatch/gouuid"
|
"github.com/nu7hatch/gouuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() { enabled["mock"] = NewMock }
|
||||||
|
|
||||||
type Mock struct {
|
type Mock struct {
|
||||||
containers container.Containers
|
containers container.Containers
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMock() *Mock {
|
func NewMock() (Connector, error) {
|
||||||
cs := &Mock{}
|
cs := &Mock{}
|
||||||
go cs.Init()
|
go cs.Init()
|
||||||
go cs.Loop()
|
go cs.Loop()
|
||||||
return cs
|
return cs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create Mock containers
|
// Create Mock containers
|
||||||
@@ -29,20 +32,46 @@ func (cs *Mock) Init() {
|
|||||||
rand.Seed(int64(time.Now().Nanosecond()))
|
rand.Seed(int64(time.Now().Nanosecond()))
|
||||||
|
|
||||||
for i := 0; i < 4; i++ {
|
for i := 0; i < 4; i++ {
|
||||||
cs.makeContainer(3)
|
cs.makeContainer(3, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < 16; i++ {
|
for i := 0; i < 16; i++ {
|
||||||
cs.makeContainer(1)
|
cs.makeContainer(1, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cs *Mock) makeContainer(aggression int64) {
|
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)
|
collector := collector.NewMock(aggression)
|
||||||
c := container.New(makeID(), collector)
|
manager := manager.NewMock()
|
||||||
|
c := container.New(makeID(), collector, manager)
|
||||||
c.SetMeta("name", makeName())
|
c.SetMeta("name", makeName())
|
||||||
c.SetState(makeState())
|
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)
|
cs.containers = append(cs.containers, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +98,7 @@ func (cs *Mock) Get(id string) (*container.Container, bool) {
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return array of all containers, sorted by field
|
// All returns array of all containers, sorted by field
|
||||||
func (cs *Mock) All() container.Containers {
|
func (cs *Mock) All() container.Containers {
|
||||||
cs.containers.Sort()
|
cs.containers.Sort()
|
||||||
cs.containers.Filter()
|
cs.containers.Filter()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// +build !darwin
|
// +build linux
|
||||||
|
|
||||||
package connector
|
package connector
|
||||||
|
|
||||||
@@ -11,11 +11,14 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bcicen/ctop/connector/collector"
|
"github.com/bcicen/ctop/connector/collector"
|
||||||
|
"github.com/bcicen/ctop/connector/manager"
|
||||||
"github.com/bcicen/ctop/container"
|
"github.com/bcicen/ctop/container"
|
||||||
"github.com/opencontainers/runc/libcontainer"
|
"github.com/opencontainers/runc/libcontainer"
|
||||||
"github.com/opencontainers/runc/libcontainer/cgroups/systemd"
|
"github.com/opencontainers/runc/libcontainer/cgroups/systemd"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func init() { enabled["runc"] = NewRunc }
|
||||||
|
|
||||||
type RuncOpts struct {
|
type RuncOpts struct {
|
||||||
root string // runc root path
|
root string // runc root path
|
||||||
systemdCgroups bool // use systemd cgroups
|
systemdCgroups bool // use systemd cgroups
|
||||||
@@ -51,35 +54,44 @@ type Runc struct {
|
|||||||
factory libcontainer.Factory
|
factory libcontainer.Factory
|
||||||
containers map[string]*container.Container
|
containers map[string]*container.Container
|
||||||
libContainers map[string]libcontainer.Container
|
libContainers map[string]libcontainer.Container
|
||||||
|
closed chan struct{}
|
||||||
needsRefresh chan string // container IDs requiring refresh
|
needsRefresh chan string // container IDs requiring refresh
|
||||||
lock sync.RWMutex
|
lock sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRunc() Connector {
|
func NewRunc() (Connector, error) {
|
||||||
opts, err := NewRuncOpts()
|
opts, err := NewRuncOpts()
|
||||||
runcFailOnErr(err)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
factory, err := getFactory(opts)
|
factory, err := getFactory(opts)
|
||||||
runcFailOnErr(err)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
cm := &Runc{
|
cm := &Runc{
|
||||||
opts: opts,
|
opts: opts,
|
||||||
factory: factory,
|
factory: factory,
|
||||||
containers: make(map[string]*container.Container),
|
containers: make(map[string]*container.Container),
|
||||||
libContainers: make(map[string]libcontainer.Container),
|
libContainers: make(map[string]libcontainer.Container),
|
||||||
needsRefresh: make(chan string, 60),
|
closed: make(chan struct{}),
|
||||||
lock: sync.RWMutex{},
|
lock: sync.RWMutex{},
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
|
select {
|
||||||
|
case <-cm.closed:
|
||||||
|
return
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
cm.refreshAll()
|
cm.refreshAll()
|
||||||
time.Sleep(5 * time.Second)
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
go cm.Loop()
|
go cm.Loop()
|
||||||
|
|
||||||
return cm
|
return cm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cm *Runc) GetLibc(id string) libcontainer.Container {
|
func (cm *Runc) GetLibc(id string) libcontainer.Container {
|
||||||
@@ -138,7 +150,11 @@ func (cm *Runc) refresh(id string) {
|
|||||||
// Read runc root, creating any new containers
|
// Read runc root, creating any new containers
|
||||||
func (cm *Runc) refreshAll() {
|
func (cm *Runc) refreshAll() {
|
||||||
list, err := ioutil.ReadDir(cm.opts.root)
|
list, err := ioutil.ReadDir(cm.opts.root)
|
||||||
runcFailOnErr(err)
|
if err != nil {
|
||||||
|
log.Errorf("%s (%T)", err.Error(), err)
|
||||||
|
close(cm.closed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
for _, i := range list {
|
for _, i := range list {
|
||||||
if i.IsDir() {
|
if i.IsDir() {
|
||||||
@@ -165,7 +181,7 @@ func (cm *Runc) Loop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a single ctop container in the map matching libc container, creating one anew if not existing
|
// 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 {
|
func (cm *Runc) MustGet(id string) *container.Container {
|
||||||
c, ok := cm.Get(id)
|
c, ok := cm.Get(id)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -175,7 +191,8 @@ func (cm *Runc) MustGet(id string) *container.Container {
|
|||||||
collector := collector.NewRunc(libc)
|
collector := collector.NewRunc(libc)
|
||||||
|
|
||||||
// create container
|
// create container
|
||||||
c = container.New(id, collector)
|
manager := manager.NewRunc()
|
||||||
|
c = container.New(id, collector, manager)
|
||||||
|
|
||||||
name := libc.ID()
|
name := libc.ID()
|
||||||
// set initial metadata
|
// set initial metadata
|
||||||
@@ -195,14 +212,6 @@ func (cm *Runc) MustGet(id string) *container.Container {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a single container, by ID
|
|
||||||
func (cm *Runc) Get(id string) (*container.Container, bool) {
|
|
||||||
cm.lock.Lock()
|
|
||||||
defer cm.lock.Unlock()
|
|
||||||
c, ok := cm.containers[id]
|
|
||||||
return c, ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove containers by ID
|
// Remove containers by ID
|
||||||
func (cm *Runc) delByID(id string) {
|
func (cm *Runc) delByID(id string) {
|
||||||
cm.lock.Lock()
|
cm.lock.Lock()
|
||||||
@@ -212,7 +221,18 @@ func (cm *Runc) delByID(id string) {
|
|||||||
log.Infof("removed dead container: %s", id)
|
log.Infof("removed dead container: %s", id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return array of all containers, sorted by field
|
// 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) {
|
func (cm *Runc) All() (containers container.Containers) {
|
||||||
cm.lock.Lock()
|
cm.lock.Lock()
|
||||||
for _, c := range cm.containers {
|
for _, c := range cm.containers {
|
||||||
@@ -227,7 +247,7 @@ func (cm *Runc) All() (containers container.Containers) {
|
|||||||
func getFactory(opts RuncOpts) (libcontainer.Factory, error) {
|
func getFactory(opts RuncOpts) (libcontainer.Factory, error) {
|
||||||
cgroupManager := libcontainer.Cgroupfs
|
cgroupManager := libcontainer.Cgroupfs
|
||||||
if opts.systemdCgroups {
|
if opts.systemdCgroups {
|
||||||
if systemd.UseSystemd() {
|
if systemd.IsRunningSystemd() {
|
||||||
cgroupManager = libcontainer.SystemdCgroups
|
cgroupManager = libcontainer.SystemdCgroups
|
||||||
} else {
|
} else {
|
||||||
return nil, fmt.Errorf("systemd cgroup enabled, but systemd support for managing cgroups is not available")
|
return nil, fmt.Errorf("systemd cgroup enabled, but systemd support for managing cgroups is not available")
|
||||||
@@ -235,9 +255,3 @@ func getFactory(opts RuncOpts) (libcontainer.Factory, error) {
|
|||||||
}
|
}
|
||||||
return libcontainer.New(opts.root, cgroupManager)
|
return libcontainer.New(opts.root, cgroupManager)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runcFailOnErr(err error) {
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Errorf("fatal runc error: %s", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package container
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/bcicen/ctop/connector/collector"
|
"github.com/bcicen/ctop/connector/collector"
|
||||||
|
"github.com/bcicen/ctop/connector/manager"
|
||||||
"github.com/bcicen/ctop/cwidgets"
|
"github.com/bcicen/ctop/cwidgets"
|
||||||
"github.com/bcicen/ctop/cwidgets/compact"
|
"github.com/bcicen/ctop/cwidgets/compact"
|
||||||
"github.com/bcicen/ctop/logging"
|
"github.com/bcicen/ctop/logging"
|
||||||
@@ -12,62 +13,69 @@ var (
|
|||||||
log = logging.Init()
|
log = logging.Init()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
running = "running"
|
||||||
|
)
|
||||||
|
|
||||||
// Metrics and metadata representing a container
|
// Metrics and metadata representing a container
|
||||||
type Container struct {
|
type Container struct {
|
||||||
models.Metrics
|
models.Metrics
|
||||||
Id string
|
Id string
|
||||||
Meta map[string]string
|
Meta models.Meta
|
||||||
Widgets *compact.Compact
|
Widgets *compact.CompactRow
|
||||||
Display bool // display this container in compact view
|
Display bool // display this container in compact view
|
||||||
updater cwidgets.WidgetUpdater
|
updater cwidgets.WidgetUpdater
|
||||||
collector collector.Collector
|
collector collector.Collector
|
||||||
|
manager manager.Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(id string, collector collector.Collector) *Container {
|
func New(id string, collector collector.Collector, manager manager.Manager) *Container {
|
||||||
widgets := compact.NewCompact(id)
|
widgets := compact.NewCompactRow()
|
||||||
return &Container{
|
return &Container{
|
||||||
Metrics: models.NewMetrics(),
|
Metrics: models.NewMetrics(),
|
||||||
Id: id,
|
Id: id,
|
||||||
Meta: make(map[string]string),
|
Meta: models.NewMeta("id", id),
|
||||||
Widgets: widgets,
|
Widgets: widgets,
|
||||||
updater: widgets,
|
updater: widgets,
|
||||||
collector: collector,
|
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) {
|
func (c *Container) SetUpdater(u cwidgets.WidgetUpdater) {
|
||||||
c.updater = u
|
c.updater = u
|
||||||
for k, v := range c.Meta {
|
c.updater.SetMeta(c.Meta)
|
||||||
c.updater.SetMeta(k, v)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) SetMeta(k, v string) {
|
func (c *Container) SetMeta(k, v string) {
|
||||||
c.Meta[k] = v
|
c.Meta[k] = v
|
||||||
c.updater.SetMeta(k, v)
|
c.updater.SetMeta(c.Meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) GetMeta(k string) string {
|
func (c *Container) GetMeta(k string) string {
|
||||||
if v, ok := c.Meta[k]; ok {
|
return c.Meta.Get(k)
|
||||||
return v
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Container) SetState(s string) {
|
func (c *Container) SetState(s string) {
|
||||||
c.SetMeta("state", s)
|
c.SetMeta("state", s)
|
||||||
// start collector, if needed
|
// start collector, if needed
|
||||||
if s == "running" && !c.collector.Running() {
|
if s == running && !c.collector.Running() {
|
||||||
c.collector.Start()
|
c.collector.Start()
|
||||||
c.Read(c.collector.Stream())
|
c.Read(c.collector.Stream())
|
||||||
}
|
}
|
||||||
// stop collector, if needed
|
// stop collector, if needed
|
||||||
if s != "running" && c.collector.Running() {
|
if s != running && c.collector.Running() {
|
||||||
c.collector.Stop()
|
c.collector.Stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return container log collector
|
// Logs returns container log collector
|
||||||
func (c *Container) Logs() collector.LogCollector {
|
func (c *Container) Logs() collector.LogCollector {
|
||||||
return c.collector.Logs()
|
return c.collector.Logs()
|
||||||
}
|
}
|
||||||
@@ -85,3 +93,68 @@ func (c *Container) Read(stream chan models.Metrics) {
|
|||||||
}()
|
}()
|
||||||
log.Infof("reader started for container: %s", c.Id)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
59
cursor.go
59
cursor.go
@@ -11,7 +11,7 @@ import (
|
|||||||
type GridCursor struct {
|
type GridCursor struct {
|
||||||
selectedID string // id of currently selected container
|
selectedID string // id of currently selected container
|
||||||
filtered container.Containers
|
filtered container.Containers
|
||||||
cSource connector.Connector
|
cSuper *connector.ConnectorSuper
|
||||||
isScrolling bool // toggled when actively scrolling
|
isScrolling bool // toggled when actively scrolling
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,14 +25,20 @@ func (gc *GridCursor) Selected() *container.Container {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh containers from source
|
// Refresh containers from source, returning whether the quantity of
|
||||||
func (gc *GridCursor) RefreshContainers() (lenChanged bool) {
|
// containers has changed and any error
|
||||||
|
func (gc *GridCursor) RefreshContainers() (bool, error) {
|
||||||
oldLen := gc.Len()
|
oldLen := gc.Len()
|
||||||
|
|
||||||
// Containers filtered by display bool
|
|
||||||
gc.filtered = container.Containers{}
|
gc.filtered = container.Containers{}
|
||||||
|
|
||||||
|
cSource, err := gc.cSuper.Get()
|
||||||
|
if err != nil {
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter Containers by display bool
|
||||||
var cursorVisible bool
|
var cursorVisible bool
|
||||||
for _, c := range gc.cSource.All() {
|
for _, c := range cSource.All() {
|
||||||
if c.Display {
|
if c.Display {
|
||||||
if c.Id == gc.selectedID {
|
if c.Id == gc.selectedID {
|
||||||
cursorVisible = true
|
cursorVisible = true
|
||||||
@@ -41,31 +47,30 @@ func (gc *GridCursor) RefreshContainers() (lenChanged bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if oldLen != gc.Len() {
|
if !cursorVisible || gc.selectedID == "" {
|
||||||
lenChanged = true
|
gc.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cursorVisible {
|
return oldLen != gc.Len(), nil
|
||||||
gc.Reset()
|
|
||||||
}
|
|
||||||
if gc.selectedID == "" {
|
|
||||||
gc.Reset()
|
|
||||||
}
|
|
||||||
return lenChanged
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set an initial cursor position, if possible
|
// Set an initial cursor position, if possible
|
||||||
func (gc *GridCursor) Reset() {
|
func (gc *GridCursor) Reset() {
|
||||||
for _, c := range gc.cSource.All() {
|
cSource, err := gc.cSuper.Get()
|
||||||
c.Widgets.Name.UnHighlight()
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cSource.All() {
|
||||||
|
c.Widgets.UnHighlight()
|
||||||
}
|
}
|
||||||
if gc.Len() > 0 {
|
if gc.Len() > 0 {
|
||||||
gc.selectedID = gc.filtered[0].Id
|
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 {
|
func (gc *GridCursor) Idx() int {
|
||||||
for n, c := range gc.filtered {
|
for n, c := range gc.filtered {
|
||||||
if c.Id == gc.selectedID {
|
if c.Id == gc.selectedID {
|
||||||
@@ -109,9 +114,9 @@ func (gc *GridCursor) Up() {
|
|||||||
active := gc.filtered[idx]
|
active := gc.filtered[idx]
|
||||||
next := gc.filtered[idx-1]
|
next := gc.filtered[idx-1]
|
||||||
|
|
||||||
active.Widgets.Name.UnHighlight()
|
active.Widgets.UnHighlight()
|
||||||
gc.selectedID = next.Id
|
gc.selectedID = next.Id
|
||||||
next.Widgets.Name.Highlight()
|
next.Widgets.Highlight()
|
||||||
|
|
||||||
gc.ScrollPage()
|
gc.ScrollPage()
|
||||||
ui.Render(cGrid)
|
ui.Render(cGrid)
|
||||||
@@ -128,9 +133,9 @@ func (gc *GridCursor) Down() {
|
|||||||
active := gc.filtered[idx]
|
active := gc.filtered[idx]
|
||||||
next := gc.filtered[idx+1]
|
next := gc.filtered[idx+1]
|
||||||
|
|
||||||
active.Widgets.Name.UnHighlight()
|
active.Widgets.UnHighlight()
|
||||||
gc.selectedID = next.Id
|
gc.selectedID = next.Id
|
||||||
next.Widgets.Name.Highlight()
|
next.Widgets.Highlight()
|
||||||
|
|
||||||
gc.ScrollPage()
|
gc.ScrollPage()
|
||||||
ui.Render(cGrid)
|
ui.Render(cGrid)
|
||||||
@@ -151,9 +156,9 @@ func (gc *GridCursor) PgUp() {
|
|||||||
active := gc.filtered[idx]
|
active := gc.filtered[idx]
|
||||||
next := gc.filtered[nextidx]
|
next := gc.filtered[nextidx]
|
||||||
|
|
||||||
active.Widgets.Name.UnHighlight()
|
active.Widgets.UnHighlight()
|
||||||
gc.selectedID = next.Id
|
gc.selectedID = next.Id
|
||||||
next.Widgets.Name.Highlight()
|
next.Widgets.Highlight()
|
||||||
|
|
||||||
cGrid.Align()
|
cGrid.Align()
|
||||||
ui.Render(cGrid)
|
ui.Render(cGrid)
|
||||||
@@ -174,9 +179,9 @@ func (gc *GridCursor) PgDown() {
|
|||||||
active := gc.filtered[idx]
|
active := gc.filtered[idx]
|
||||||
next := gc.filtered[nextidx]
|
next := gc.filtered[nextidx]
|
||||||
|
|
||||||
active.Widgets.Name.UnHighlight()
|
active.Widgets.UnHighlight()
|
||||||
gc.selectedID = next.Id
|
gc.selectedID = next.Id
|
||||||
next.Widgets.Name.Highlight()
|
next.Widgets.Highlight()
|
||||||
|
|
||||||
cGrid.Align()
|
cGrid.Align()
|
||||||
ui.Render(cGrid)
|
ui.Render(cGrid)
|
||||||
|
|||||||
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
|
package compact
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/bcicen/ctop/cwidgets"
|
||||||
|
"github.com/bcicen/ctop/models"
|
||||||
|
|
||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GaugeCol struct {
|
type CPUCol struct {
|
||||||
*ui.Gauge
|
*GaugeCol
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGaugeCol() *GaugeCol {
|
func NewCPUCol() CompactCol {
|
||||||
g := ui.NewGauge()
|
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.Height = 1
|
||||||
g.Border = false
|
g.Border = false
|
||||||
g.Percent = 0
|
|
||||||
g.PaddingBottom = 0
|
g.PaddingBottom = 0
|
||||||
g.Label = "-"
|
g.Reset()
|
||||||
return &GaugeCol{g}
|
return g
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *GaugeCol) Reset() {
|
func (w *GaugeCol) Reset() {
|
||||||
@@ -23,12 +62,41 @@ func (w *GaugeCol) Reset() {
|
|||||||
w.Percent = 0
|
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 {
|
func colorScale(n int) ui.Attribute {
|
||||||
if n > 70 {
|
if n > 70 {
|
||||||
return ui.ColorRed
|
return ui.ThemeAttr("status.danger")
|
||||||
}
|
}
|
||||||
if n > 30 {
|
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"
|
ui "github.com/gizak/termui"
|
||||||
)
|
)
|
||||||
|
|
||||||
var header *CompactHeader
|
|
||||||
|
|
||||||
type CompactGrid struct {
|
type CompactGrid struct {
|
||||||
ui.GridBufferer
|
ui.GridBufferer
|
||||||
Rows []ui.GridBufferer
|
header *CompactHeader
|
||||||
|
cols []CompactCol // reference columns
|
||||||
|
Rows []RowBufferer
|
||||||
X, Y int
|
X, Y int
|
||||||
Width int
|
Width int
|
||||||
Height int
|
Height int
|
||||||
@@ -16,37 +16,64 @@ type CompactGrid struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewCompactGrid() *CompactGrid {
|
func NewCompactGrid() *CompactGrid {
|
||||||
header = NewCompactHeader() // init column header
|
cg := &CompactGrid{header: NewCompactHeader()}
|
||||||
return &CompactGrid{}
|
cg.rebuildHeader()
|
||||||
|
return cg
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cg *CompactGrid) Align() {
|
func (cg *CompactGrid) Align() {
|
||||||
y := cg.Y
|
y := cg.Y
|
||||||
|
|
||||||
if cg.Offset >= len(cg.Rows) {
|
if cg.Offset >= len(cg.Rows) || cg.Offset < 0 {
|
||||||
cg.Offset = 0
|
|
||||||
}
|
|
||||||
if cg.Offset < 0 {
|
|
||||||
cg.Offset = 0
|
cg.Offset = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// update row ypos, width recursively
|
// update row ypos, width recursively
|
||||||
|
colWidths := cg.calcWidths()
|
||||||
for _, r := range cg.pageRows() {
|
for _, r := range cg.pageRows() {
|
||||||
r.SetY(y)
|
r.SetY(y)
|
||||||
y += r.GetHeight()
|
y += r.GetHeight()
|
||||||
r.SetWidth(cg.Width)
|
r.SetWidths(cg.Width, colWidths)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cg *CompactGrid) Clear() { cg.Rows = []ui.GridBufferer{} }
|
func (cg *CompactGrid) Clear() {
|
||||||
func (cg *CompactGrid) GetHeight() int { return len(cg.Rows) + header.Height }
|
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) SetX(x int) { cg.X = x }
|
||||||
func (cg *CompactGrid) SetY(y int) { cg.Y = y }
|
func (cg *CompactGrid) SetY(y int) { cg.Y = y }
|
||||||
func (cg *CompactGrid) SetWidth(w int) { cg.Width = w }
|
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) {
|
// calculate and return per-column width
|
||||||
rows = append(rows, header)
|
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:]...)
|
rows = append(rows, cg.Rows[cg.Offset:]...)
|
||||||
return rows
|
return rows
|
||||||
}
|
}
|
||||||
@@ -59,8 +86,14 @@ func (cg *CompactGrid) Buffer() ui.Buffer {
|
|||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cg *CompactGrid) AddRows(rows ...ui.GridBufferer) {
|
func (cg *CompactGrid) AddRows(rows ...RowBufferer) {
|
||||||
for _, r := range rows {
|
cg.Rows = append(cg.Rows, rows...)
|
||||||
cg.Rows = append(cg.Rows, r)
|
}
|
||||||
|
|
||||||
|
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
|
X, Y int
|
||||||
Width int
|
Width int
|
||||||
Height int
|
Height int
|
||||||
|
cols []CompactCol
|
||||||
|
widths []int
|
||||||
pars []*ui.Par
|
pars []*ui.Par
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCompactHeader() *CompactHeader {
|
func NewCompactHeader() *CompactHeader {
|
||||||
fields := []string{"", "NAME", "CID", "CPU", "MEM", "NET RX/TX", "IO R/W", "PIDS"}
|
return &CompactHeader{
|
||||||
ch := &CompactHeader{}
|
X: rowPadding,
|
||||||
ch.Height = 2
|
Height: 2,
|
||||||
for _, f := range fields {
|
|
||||||
ch.addFieldPar(f)
|
|
||||||
}
|
}
|
||||||
return ch
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch *CompactHeader) GetHeight() int {
|
func (row *CompactHeader) GetHeight() int {
|
||||||
return ch.Height
|
return row.Height
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch *CompactHeader) SetWidth(w int) {
|
func (row *CompactHeader) SetWidths(totalWidth int, widths []int) {
|
||||||
x := ch.X
|
x := row.X
|
||||||
autoWidth := calcWidth(w)
|
|
||||||
for n, col := range ch.pars {
|
for n, w := range row.pars {
|
||||||
// set column to static width
|
w.SetX(x)
|
||||||
if colWidths[n] != 0 {
|
w.SetWidth(widths[n])
|
||||||
col.SetX(x)
|
x += widths[n] + colSpacing
|
||||||
col.SetWidth(colWidths[n])
|
|
||||||
x += colWidths[n]
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
col.SetX(x)
|
row.Width = totalWidth
|
||||||
col.SetWidth(autoWidth)
|
|
||||||
x += autoWidth + colSpacing
|
|
||||||
}
|
|
||||||
ch.Width = w
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch *CompactHeader) SetX(x int) {
|
func (row *CompactHeader) SetX(x int) {
|
||||||
ch.X = x
|
row.X = x
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch *CompactHeader) SetY(y int) {
|
func (row *CompactHeader) SetY(y int) {
|
||||||
for _, p := range ch.pars {
|
for _, p := range row.pars {
|
||||||
p.SetY(y)
|
p.SetY(y)
|
||||||
}
|
}
|
||||||
ch.Y = y
|
row.Y = y
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch *CompactHeader) Buffer() ui.Buffer {
|
func (row *CompactHeader) Buffer() ui.Buffer {
|
||||||
buf := ui.NewBuffer()
|
buf := ui.NewBuffer()
|
||||||
for _, p := range ch.pars {
|
for _, p := range row.pars {
|
||||||
buf.Merge(p.Buffer())
|
buf.Merge(p.Buffer())
|
||||||
}
|
}
|
||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ch *CompactHeader) addFieldPar(s string) {
|
func (row *CompactHeader) clearFieldPars() {
|
||||||
p := ui.NewPar(s)
|
row.pars = []*ui.Par{}
|
||||||
p.Height = ch.Height
|
}
|
||||||
p.Border = false
|
|
||||||
ch.pars = append(ch.pars, p)
|
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/models"
|
|
||||||
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 models.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
|
package compact
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"github.com/bcicen/ctop/models"
|
||||||
|
|
||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
mark = string('\u25C9')
|
mark = "◉"
|
||||||
vBar = string('\u25AE')
|
healthMark = "✚"
|
||||||
statusWidth = 3
|
vBar = string('\u25AE') + string('\u25AE')
|
||||||
)
|
)
|
||||||
|
|
||||||
// Status indicator
|
// Status indicator
|
||||||
type Status struct {
|
type Status struct {
|
||||||
*ui.Par
|
*ui.Block
|
||||||
|
status []ui.Cell
|
||||||
|
health []ui.Cell
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStatus() *Status {
|
func NewStatus() CompactCol {
|
||||||
p := ui.NewPar(mark)
|
s := &Status{
|
||||||
p.Border = false
|
Block: ui.NewBlock(),
|
||||||
p.Height = 1
|
health: []ui.Cell{{Ch: ' '}},
|
||||||
p.Width = statusWidth
|
}
|
||||||
return &Status{p}
|
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
|
// defaults
|
||||||
text := mark
|
text := mark
|
||||||
color := ui.ColorDefault
|
color := ui.ColorDefault
|
||||||
|
|
||||||
switch val {
|
switch val {
|
||||||
case "running":
|
case "running":
|
||||||
color = ui.ColorGreen
|
color = ui.ThemeAttr("status.ok")
|
||||||
case "exited":
|
case "exited":
|
||||||
color = ui.ColorRed
|
color = ui.ThemeAttr("status.danger")
|
||||||
case "paused":
|
case "paused":
|
||||||
text = fmt.Sprintf("%s%s", vBar, vBar)
|
text = vBar
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Text = text
|
s.status = ui.TextCells(text, color, ui.ColorDefault)
|
||||||
s.TextFgColor = color
|
}
|
||||||
|
|
||||||
|
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
|
package compact
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/bcicen/ctop/cwidgets"
|
||||||
|
"github.com/bcicen/ctop/models"
|
||||||
|
|
||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TextCol struct {
|
type NameCol struct {
|
||||||
*ui.Par
|
*TextCol
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTextCol(s string) *TextCol {
|
func NewNameCol() CompactCol {
|
||||||
p := ui.NewPar(s)
|
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.Border = false
|
||||||
p.Height = 1
|
p.Height = 1
|
||||||
p.Width = 20
|
p.Width = 20
|
||||||
return &TextCol{p}
|
|
||||||
|
return &TextCol{
|
||||||
|
Par: p,
|
||||||
|
header: header,
|
||||||
|
fWidth: 0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *TextCol) Highlight() {
|
func (w *TextCol) Highlight() {
|
||||||
|
w.Bg = ui.ThemeAttr("par.text.fg")
|
||||||
w.TextFgColor = ui.ThemeAttr("par.text.hi")
|
w.TextFgColor = ui.ThemeAttr("par.text.hi")
|
||||||
w.TextBgColor = ui.ThemeAttr("par.text.fg")
|
w.TextBgColor = ui.ThemeAttr("par.text.fg")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *TextCol) UnHighlight() {
|
func (w *TextCol) UnHighlight() {
|
||||||
|
w.Bg = ui.ThemeAttr("par.text.bg")
|
||||||
w.TextFgColor = ui.ThemeAttr("par.text.fg")
|
w.TextFgColor = ui.ThemeAttr("par.text.fg")
|
||||||
w.TextBgColor = ui.ThemeAttr("par.text.bg")
|
w.TextBgColor = ui.ThemeAttr("par.text.bg")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *TextCol) Reset() {
|
// TextCol implements CompactCol
|
||||||
w.Text = "-"
|
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
|
w.Text = s
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,36 +4,12 @@ package compact
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
)
|
)
|
||||||
|
|
||||||
const colSpacing = 1
|
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) {
|
func centerParText(p *ui.Par) {
|
||||||
var text string
|
var text string
|
||||||
var padding string
|
var padding string
|
||||||
|
|||||||
@@ -8,6 +8,14 @@ import (
|
|||||||
var log = logging.Init()
|
var log = logging.Init()
|
||||||
|
|
||||||
type WidgetUpdater interface {
|
type WidgetUpdater interface {
|
||||||
SetMeta(string, string)
|
SetMeta(models.Meta)
|
||||||
SetMetrics(models.Metrics)
|
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 (
|
import (
|
||||||
ui "github.com/gizak/termui"
|
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 {
|
type IntHist struct {
|
||||||
Val int // most current data point
|
Val int // most current data point
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package expanded
|
package single
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
)
|
)
|
||||||
|
|
||||||
var displayInfo = []string{"id", "name", "image", "ports", "state", "created"}
|
var displayInfo = []string{"id", "name", "image", "ports", "IPs", "state", "created", "health"}
|
||||||
|
|
||||||
type Info struct {
|
type Info struct {
|
||||||
*ui.Table
|
*ui.Table
|
||||||
@@ -45,7 +45,7 @@ func mkInfoRows(k, v string) (rows [][]string) {
|
|||||||
// initial row with field name
|
// initial row with field name
|
||||||
rows = append(rows, []string{k, lines[0]})
|
rows = append(rows, []string{k, lines[0]})
|
||||||
|
|
||||||
// append any additional lines in seperate row
|
// append any additional lines in separate row
|
||||||
if len(lines) > 1 {
|
if len(lines) > 1 {
|
||||||
for _, line := range lines[1:] {
|
for _, line := range lines[1:] {
|
||||||
if line != "" {
|
if line != "" {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package expanded
|
package single
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -42,10 +42,10 @@ func (w *IO) Update(read int64, write int64) {
|
|||||||
var rate string
|
var rate string
|
||||||
|
|
||||||
w.readHist.Append(int(read))
|
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.Lines[0].Title = fmt.Sprintf("read [%s/s]", rate)
|
||||||
|
|
||||||
w.writeHist.Append(int(write))
|
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)
|
w.Lines[1].Title = fmt.Sprintf("write [%s/s]", rate)
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package expanded
|
package single
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package expanded
|
package single
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/bcicen/ctop/logging"
|
"github.com/bcicen/ctop/logging"
|
||||||
@@ -12,31 +12,33 @@ var (
|
|||||||
colWidth = [2]int{65, 0} // left,right column width
|
colWidth = [2]int{65, 0} // left,right column width
|
||||||
)
|
)
|
||||||
|
|
||||||
type Expanded struct {
|
type Single struct {
|
||||||
Info *Info
|
Info *Info
|
||||||
Net *Net
|
Net *Net
|
||||||
Cpu *Cpu
|
Cpu *Cpu
|
||||||
Mem *Mem
|
Mem *Mem
|
||||||
IO *IO
|
IO *IO
|
||||||
|
Env *Env
|
||||||
X, Y int
|
X, Y int
|
||||||
Width int
|
Width int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewExpanded(id string) *Expanded {
|
func NewSingle(id string) *Single {
|
||||||
if len(id) > 12 {
|
if len(id) > 12 {
|
||||||
id = id[:12]
|
id = id[:12]
|
||||||
}
|
}
|
||||||
return &Expanded{
|
return &Single{
|
||||||
Info: NewInfo(id),
|
Info: NewInfo(id),
|
||||||
Net: NewNet(),
|
Net: NewNet(),
|
||||||
Cpu: NewCpu(),
|
Cpu: NewCpu(),
|
||||||
Mem: NewMem(),
|
Mem: NewMem(),
|
||||||
IO: NewIO(),
|
IO: NewIO(),
|
||||||
|
Env: NewEnv(),
|
||||||
Width: ui.TermWidth(),
|
Width: ui.TermWidth(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Expanded) Up() {
|
func (e *Single) Up() {
|
||||||
if e.Y < 0 {
|
if e.Y < 0 {
|
||||||
e.Y++
|
e.Y++
|
||||||
e.Align()
|
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()) {
|
if e.Y > (ui.TermHeight() - e.GetHeight()) {
|
||||||
e.Y--
|
e.Y--
|
||||||
e.Align()
|
e.Align()
|
||||||
@@ -52,27 +54,36 @@ func (e *Expanded) Down() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Expanded) SetWidth(w int) { e.Width = w }
|
func (e *Single) SetWidth(w int) { e.Width = w }
|
||||||
func (e *Expanded) SetMeta(k, v string) { e.Info.Set(k, v) }
|
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 models.Metrics) {
|
func (e *Single) SetMetrics(m models.Metrics) {
|
||||||
e.Cpu.Update(m.CPUUtil)
|
e.Cpu.Update(m.CPUUtil)
|
||||||
e.Net.Update(m.NetRx, m.NetTx)
|
e.Net.Update(m.NetRx, m.NetTx)
|
||||||
e.Mem.Update(int(m.MemUsage), int(m.MemLimit))
|
e.Mem.Update(int(m.MemUsage), int(m.MemLimit))
|
||||||
e.IO.Update(m.IOBytesRead, m.IOBytesWrite)
|
e.IO.Update(m.IOBytesRead, m.IOBytesWrite)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return total column height
|
// GetHeight returns total column height
|
||||||
func (e *Expanded) GetHeight() (h int) {
|
func (e *Single) GetHeight() (h int) {
|
||||||
h += e.Info.Height
|
h += e.Info.Height
|
||||||
h += e.Net.Height
|
h += e.Net.Height
|
||||||
h += e.Cpu.Height
|
h += e.Cpu.Height
|
||||||
h += e.Mem.Height
|
h += e.Mem.Height
|
||||||
h += e.IO.Height
|
h += e.IO.Height
|
||||||
|
h += e.Env.Height
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Expanded) Align() {
|
func (e *Single) Align() {
|
||||||
// reset offset if needed
|
// reset offset if needed
|
||||||
if e.GetHeight() <= ui.TermHeight() {
|
if e.GetHeight() <= ui.TermHeight() {
|
||||||
e.Y = 0
|
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])
|
log.Debugf("align: width=%v left-col=%v right-col=%v", e.Width, colWidth[0], colWidth[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
func calcWidth(w int) {
|
func (e *Single) Buffer() ui.Buffer {
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Expanded) Buffer() ui.Buffer {
|
|
||||||
buf := ui.NewBuffer()
|
buf := ui.NewBuffer()
|
||||||
if e.Width < (colWidth[0] + colWidth[1]) {
|
if e.Width < (colWidth[0] + colWidth[1]) {
|
||||||
ui.Clear()
|
ui.Clear()
|
||||||
@@ -106,16 +114,18 @@ func (e *Expanded) Buffer() ui.Buffer {
|
|||||||
buf.Merge(e.Mem.Buffer())
|
buf.Merge(e.Mem.Buffer())
|
||||||
buf.Merge(e.Net.Buffer())
|
buf.Merge(e.Net.Buffer())
|
||||||
buf.Merge(e.IO.Buffer())
|
buf.Merge(e.IO.Buffer())
|
||||||
|
buf.Merge(e.Env.Buffer())
|
||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Expanded) all() []ui.GridBufferer {
|
func (e *Single) all() []ui.GridBufferer {
|
||||||
return []ui.GridBufferer{
|
return []ui.GridBufferer{
|
||||||
e.Info,
|
e.Info,
|
||||||
e.Cpu,
|
e.Cpu,
|
||||||
e.Mem,
|
e.Mem,
|
||||||
e.Net,
|
e.Net,
|
||||||
e.IO,
|
e.IO,
|
||||||
|
e.Env,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package expanded
|
package single
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -70,7 +70,7 @@ func newMemChart() *ui.MBarChart {
|
|||||||
mbar.BarColor[1] = ui.ColorBlack
|
mbar.BarColor[1] = ui.ColorBlack
|
||||||
mbar.NumColor[1] = ui.ColorBlack
|
mbar.NumColor[1] = ui.ColorBlack
|
||||||
|
|
||||||
mbar.NumFmt = cwidgets.ByteFormatInt
|
mbar.NumFmt = cwidgets.ByteFormatShort
|
||||||
//mbar.ShowScale = true
|
//mbar.ShowScale = true
|
||||||
return mbar
|
return mbar
|
||||||
}
|
}
|
||||||
@@ -78,6 +78,6 @@ func newMemChart() *ui.MBarChart {
|
|||||||
func (w *Mem) Update(val int, limit int) {
|
func (w *Mem) Update(val int, limit int) {
|
||||||
w.valHist.Append(val)
|
w.valHist.Append(val)
|
||||||
w.limitHist.Append(limit - 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
|
//w.Data[0] = w.hist.data
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package expanded
|
package single
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -42,10 +42,10 @@ func (w *Net) Update(rx int64, tx int64) {
|
|||||||
var rate string
|
var rate string
|
||||||
|
|
||||||
w.rxHist.Append(int(rx))
|
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.Lines[0].Title = fmt.Sprintf("RX [%s/s]", rate)
|
||||||
|
|
||||||
w.txHist.Append(int(tx))
|
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)
|
w.Lines[1].Title = fmt.Sprintf("TX [%s/s]", rate)
|
||||||
}
|
}
|
||||||
@@ -1,53 +1,74 @@
|
|||||||
package cwidgets
|
package cwidgets
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
kb = 1024
|
// byte ratio constants
|
||||||
mb = kb * 1024
|
_ = iota
|
||||||
gb = mb * 1024
|
kib float64 = 1 << (10 * iota)
|
||||||
tb = gb * 1024
|
mib
|
||||||
|
gib
|
||||||
|
tib
|
||||||
|
pib
|
||||||
)
|
)
|
||||||
|
|
||||||
// convenience method
|
var (
|
||||||
func ByteFormatInt(n int) string {
|
units = []float64{
|
||||||
return ByteFormat(int64(n))
|
1,
|
||||||
|
kib,
|
||||||
|
mib,
|
||||||
|
gib,
|
||||||
|
tib,
|
||||||
|
pib,
|
||||||
}
|
}
|
||||||
|
|
||||||
func ByteFormat(n int64) string {
|
// short, full unit labels
|
||||||
if n < kb {
|
labels = [][2]string{
|
||||||
return fmt.Sprintf("%sB", strconv.FormatInt(n, 10))
|
[2]string{"B", "B"},
|
||||||
|
[2]string{"K", "KiB"},
|
||||||
|
[2]string{"M", "MiB"},
|
||||||
|
[2]string{"G", "GiB"},
|
||||||
|
[2]string{"T", "TiB"},
|
||||||
|
[2]string{"P", "PiB"},
|
||||||
}
|
}
|
||||||
if n < mb {
|
)
|
||||||
n = n / kb
|
|
||||||
return fmt.Sprintf("%sK", strconv.FormatInt(n, 10))
|
// 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
|
||||||
}
|
}
|
||||||
if n < gb {
|
i--
|
||||||
n = n / mb
|
|
||||||
return fmt.Sprintf("%sM", strconv.FormatInt(n, 10))
|
|
||||||
}
|
|
||||||
if n < tb {
|
|
||||||
nf := float64(n) / gb
|
|
||||||
return fmt.Sprintf("%sG", unpadFloat(nf))
|
|
||||||
}
|
|
||||||
nf := float64(n) / tb
|
|
||||||
return fmt.Sprintf("%sT", unpadFloat(nf))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func unpadFloat(f float64) string {
|
if short {
|
||||||
return strconv.FormatFloat(f, 'f', getPrecision(f), 64)
|
return unpadFloat(n, 0) + labels[i][0]
|
||||||
|
}
|
||||||
|
return unpadFloat(n, 2) + labels[i][1]
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPrecision(f float64) int {
|
func unpadFloat(f float64, maxp int) string {
|
||||||
|
return strconv.FormatFloat(f, 'f', getPrecision(f, maxp), 64)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPrecision(f float64, maxp int) int {
|
||||||
frac := int((f - float64(int(f))) * 100)
|
frac := int((f - float64(int(f))) * 100)
|
||||||
if frac == 0 {
|
if frac == 0 || maxp == 0 {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
if frac%10 == 0 {
|
if frac%10 == 0 || maxp < 2 {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
return 2 // default precision
|
return maxp
|
||||||
}
|
}
|
||||||
|
|||||||
129
glide.lock
generated
129
glide.lock
generated
@@ -1,129 +0,0 @@
|
|||||||
hash: 0d550b01b3a1c4751a8f5c3fba0c43f62252055e231712729628e514bb494da8
|
|
||||||
updated: 2017-06-09T18:11:10.930196504-03:00
|
|
||||||
imports:
|
|
||||||
- name: github.com/Azure/go-ansiterm
|
|
||||||
version: fa152c58bc15761d0200cb75fe958b89a9d4888e
|
|
||||||
subpackages:
|
|
||||||
- winterm
|
|
||||||
- name: github.com/c9s/goprocinfo
|
|
||||||
version: b34328d6e0cd139894ea7347d2624ccf31fa3c58
|
|
||||||
subpackages:
|
|
||||||
- linux
|
|
||||||
- name: github.com/coreos/go-systemd
|
|
||||||
version: b4a58d95188dd092ae20072bac14cece0e67c388
|
|
||||||
subpackages:
|
|
||||||
- activation
|
|
||||||
- dbus
|
|
||||||
- util
|
|
||||||
- name: github.com/docker/docker
|
|
||||||
version: 90d35abf7b3535c1c319c872900fbd76374e521c
|
|
||||||
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/mount
|
|
||||||
- pkg/pools
|
|
||||||
- pkg/promise
|
|
||||||
- pkg/stdcopy
|
|
||||||
- pkg/symlink
|
|
||||||
- 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/godbus/dbus
|
|
||||||
version: c7fdd8b5cd55e87b4e1f4e372cdb1db61dd6c66f
|
|
||||||
- name: github.com/golang/protobuf
|
|
||||||
version: f7137ae6b19afbfd61a94b746fda3b3fe0491874
|
|
||||||
subpackages:
|
|
||||||
- proto
|
|
||||||
- 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/Nvveen/Gotty
|
|
||||||
version: cd527374f1e5bff4938207604a14f2e38a9cf512
|
|
||||||
- name: github.com/op/go-logging
|
|
||||||
version: b2cb9fa56473e98db8caba80237377e83fe44db5
|
|
||||||
- name: github.com/opencontainers/runc
|
|
||||||
version: baf6536d6259209c3edfa2b22237af82942d3dfa
|
|
||||||
subpackages:
|
|
||||||
- libcontainer
|
|
||||||
- libcontainer/apparmor
|
|
||||||
- libcontainer/cgroups
|
|
||||||
- libcontainer/cgroups/fs
|
|
||||||
- libcontainer/cgroups/systemd
|
|
||||||
- libcontainer/configs
|
|
||||||
- libcontainer/configs/validate
|
|
||||||
- libcontainer/criurpc
|
|
||||||
- libcontainer/keys
|
|
||||||
- libcontainer/label
|
|
||||||
- libcontainer/seccomp
|
|
||||||
- libcontainer/selinux
|
|
||||||
- libcontainer/stacktrace
|
|
||||||
- libcontainer/system
|
|
||||||
- libcontainer/user
|
|
||||||
- libcontainer/utils
|
|
||||||
- name: github.com/seccomp/libseccomp-golang
|
|
||||||
version: 1b506fc7c24eec5a3693cdcbed40d9c226cfc6a1
|
|
||||||
- name: github.com/Sirupsen/logrus
|
|
||||||
version: 26709e2714106fb8ad40b773b711ebce25b78914
|
|
||||||
- name: github.com/syndtr/gocapability
|
|
||||||
version: 2c00daeb6c3b45114c80ac44119e7b8801fdd852
|
|
||||||
subpackages:
|
|
||||||
- capability
|
|
||||||
- name: github.com/vishvananda/netlink
|
|
||||||
version: 1e2e08e8a2dcdacaae3f14ac44c5cfa31361f270
|
|
||||||
subpackages:
|
|
||||||
- nl
|
|
||||||
- name: golang.org/x/net
|
|
||||||
version: a6577fac2d73be281a500b310739095313165611
|
|
||||||
subpackages:
|
|
||||||
- context
|
|
||||||
- context/ctxhttp
|
|
||||||
- name: golang.org/x/sys
|
|
||||||
version: 99f16d856c9836c42d24e7ab64ea72916925fa97
|
|
||||||
subpackages:
|
|
||||||
- unix
|
|
||||||
- windows
|
|
||||||
testImports: []
|
|
||||||
18
glide.yaml
18
glide.yaml
@@ -1,18 +0,0 @@
|
|||||||
package: github.com/bcicen/ctop
|
|
||||||
import:
|
|
||||||
- package: github.com/c9s/goprocinfo/linux
|
|
||||||
- package: github.com/docker/docker
|
|
||||||
version: ^17.5.0-ce-rc3
|
|
||||||
- package: github.com/opencontainers/runc
|
|
||||||
version: 0.1.1
|
|
||||||
- package: github.com/fsouza/go-dockerclient
|
|
||||||
version: 318513eb1ab27495afbc67f671ba1080513d8aa0
|
|
||||||
- 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/c9s/goprocinfo/linux
|
|
||||||
- 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=
|
||||||
140
grid.go
140
grid.go
@@ -2,11 +2,48 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/bcicen/ctop/config"
|
"github.com/bcicen/ctop/config"
|
||||||
"github.com/bcicen/ctop/container"
|
"github.com/bcicen/ctop/cwidgets/single"
|
||||||
"github.com/bcicen/ctop/cwidgets/expanded"
|
|
||||||
ui "github.com/gizak/termui"
|
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) {
|
func RedrawRows(clr bool) {
|
||||||
// reinit body rows
|
// reinit body rows
|
||||||
cGrid.Clear()
|
cGrid.Clear()
|
||||||
@@ -18,6 +55,7 @@ func RedrawRows(clr bool) {
|
|||||||
header.SetFilter(config.GetVal("filterStr"))
|
header.SetFilter(config.GetVal("filterStr"))
|
||||||
y += header.Height()
|
y += header.Height()
|
||||||
}
|
}
|
||||||
|
|
||||||
cGrid.SetY(y)
|
cGrid.SetY(y)
|
||||||
|
|
||||||
for _, c := range cursor.filtered {
|
for _, c := range cursor.filtered {
|
||||||
@@ -35,12 +73,17 @@ func RedrawRows(clr bool) {
|
|||||||
ui.Render(cGrid)
|
ui.Render(cGrid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExpandView(c *container.Container) {
|
func SingleView() MenuFn {
|
||||||
|
c := cursor.Selected()
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
ui.Clear()
|
ui.Clear()
|
||||||
ui.DefaultEvtStream.ResetHandlers()
|
ui.DefaultEvtStream.ResetHandlers()
|
||||||
defer ui.DefaultEvtStream.ResetHandlers()
|
defer ui.DefaultEvtStream.ResetHandlers()
|
||||||
|
|
||||||
ex := expanded.NewExpanded(c.Id)
|
ex := single.NewSingle(c.Id)
|
||||||
c.SetUpdater(ex)
|
c.SetUpdater(ex)
|
||||||
|
|
||||||
ex.Align()
|
ex.Align()
|
||||||
@@ -59,25 +102,31 @@ func ExpandView(c *container.Container) {
|
|||||||
|
|
||||||
ui.Loop()
|
ui.Loop()
|
||||||
c.SetUpdater(c.Widgets)
|
c.SetUpdater(c.Widgets)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func RefreshDisplay() {
|
func RefreshDisplay() error {
|
||||||
// skip display refresh during scroll
|
// skip display refresh during scroll
|
||||||
if !cursor.isScrolling {
|
if !cursor.isScrolling {
|
||||||
needsClear := cursor.RefreshContainers()
|
needsClear, err := cursor.RefreshContainers()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
RedrawRows(needsClear)
|
RedrawRows(needsClear)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Display() bool {
|
func Display() bool {
|
||||||
var menu func()
|
var menu MenuFn
|
||||||
var expand bool
|
var connErr error
|
||||||
|
|
||||||
cGrid.SetWidth(ui.TermWidth())
|
cGrid.SetWidth(ui.TermWidth())
|
||||||
ui.DefaultEvtStream.Hook(logEvent)
|
ui.DefaultEvtStream.Hook(logEvent)
|
||||||
|
|
||||||
// initial draw
|
// initial draw
|
||||||
header.Align()
|
header.Align()
|
||||||
|
status.Align()
|
||||||
cursor.RefreshContainers()
|
cursor.RefreshContainers()
|
||||||
RedrawRows(true)
|
RedrawRows(true)
|
||||||
|
|
||||||
@@ -94,12 +143,35 @@ func Display() bool {
|
|||||||
})
|
})
|
||||||
|
|
||||||
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
|
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.StopLoop()
|
||||||
})
|
})
|
||||||
ui.Handle("/sys/kbd/a", func(ui.Event) {
|
ui.Handle("/sys/kbd/a", func(ui.Event) {
|
||||||
config.Toggle("allContainers")
|
config.Toggle("allContainers")
|
||||||
RefreshDisplay()
|
connErr = RefreshDisplay()
|
||||||
|
if connErr != nil {
|
||||||
|
ui.StopLoop()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
ui.Handle("/sys/kbd/D", func(ui.Event) {
|
ui.Handle("/sys/kbd/D", func(ui.Event) {
|
||||||
dumpContainer(cursor.Selected())
|
dumpContainer(cursor.Selected())
|
||||||
@@ -119,13 +191,33 @@ func Display() bool {
|
|||||||
menu = SortMenu
|
menu = SortMenu
|
||||||
ui.StopLoop()
|
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) {
|
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) {
|
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
|
||||||
header.Align()
|
header.Align()
|
||||||
|
status.Align()
|
||||||
cursor.ScrollPage()
|
cursor.ScrollPage()
|
||||||
cGrid.SetWidth(ui.TermWidth())
|
cGrid.SetWidth(ui.TermWidth())
|
||||||
log.Infof("resize: width=%v max-rows=%v", cGrid.Width, cGrid.MaxRows())
|
log.Infof("resize: width=%v max-rows=%v", cGrid.Width, cGrid.MaxRows())
|
||||||
@@ -133,16 +225,28 @@ func Display() bool {
|
|||||||
})
|
})
|
||||||
|
|
||||||
ui.Loop()
|
ui.Loop()
|
||||||
|
|
||||||
|
if connErr != nil {
|
||||||
|
return ShowConnError(connErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
if menu != nil {
|
||||||
menu()
|
for menu != nil {
|
||||||
return false
|
menu = menu()
|
||||||
}
|
|
||||||
if expand {
|
|
||||||
c := cursor.Selected()
|
|
||||||
if c != nil {
|
|
||||||
ExpandView(c)
|
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
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!"
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package logging
|
package logging
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -20,11 +21,36 @@ var (
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type statusMsg struct {
|
||||||
|
Text string
|
||||||
|
IsError bool
|
||||||
|
}
|
||||||
|
|
||||||
type CTopLogger struct {
|
type CTopLogger struct {
|
||||||
*logging.Logger
|
*logging.Logger
|
||||||
backend *logging.MemoryBackend
|
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 {
|
func Init() *CTopLogger {
|
||||||
if Log == nil {
|
if Log == nil {
|
||||||
logging.SetFormatter(format) // setup default formatter
|
logging.SetFormatter(format) // setup default formatter
|
||||||
@@ -32,6 +58,7 @@ func Init() *CTopLogger {
|
|||||||
Log = &CTopLogger{
|
Log = &CTopLogger{
|
||||||
logging.MustGetLogger("ctop"),
|
logging.MustGetLogger("ctop"),
|
||||||
logging.NewMemoryBackend(size),
|
logging.NewMemoryBackend(size),
|
||||||
|
[]statusMsg{},
|
||||||
}
|
}
|
||||||
|
|
||||||
if debugMode() {
|
if debugMode() {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package logging
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
@@ -56,13 +57,13 @@ func StopServer() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handler(conn net.Conn) {
|
func handler(wc io.WriteCloser) {
|
||||||
server.wg.Add(1)
|
server.wg.Add(1)
|
||||||
defer server.wg.Done()
|
defer server.wg.Done()
|
||||||
defer conn.Close()
|
defer wc.Close()
|
||||||
for msg := range Log.tail() {
|
for msg := range Log.tail() {
|
||||||
msg = fmt.Sprintf("%s\n", msg)
|
msg = fmt.Sprintf("%s\n", msg)
|
||||||
conn.Write([]byte(msg))
|
wc.Write([]byte(msg))
|
||||||
}
|
}
|
||||||
conn.Write([]byte("bye\n"))
|
wc.Write([]byte("bye\n"))
|
||||||
}
|
}
|
||||||
|
|||||||
48
main.go
48
main.go
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/bcicen/ctop/config"
|
"github.com/bcicen/ctop/config"
|
||||||
"github.com/bcicen/ctop/connector"
|
"github.com/bcicen/ctop/connector"
|
||||||
@@ -25,6 +26,8 @@ var (
|
|||||||
cursor *GridCursor
|
cursor *GridCursor
|
||||||
cGrid *compact.CompactGrid
|
cGrid *compact.CompactGrid
|
||||||
header *widgets.CTopHeader
|
header *widgets.CTopHeader
|
||||||
|
status *widgets.StatusLine
|
||||||
|
errView *widgets.ErrorView
|
||||||
|
|
||||||
versionStr = fmt.Sprintf("ctop version %v, build %v %v", version, build, goVersion)
|
versionStr = fmt.Sprintf("ctop version %v, build %v %v", version, build, goVersion)
|
||||||
)
|
)
|
||||||
@@ -33,14 +36,18 @@ func main() {
|
|||||||
defer panicExit()
|
defer panicExit()
|
||||||
|
|
||||||
// parse command line arguments
|
// parse command line arguments
|
||||||
var versionFlag = flag.Bool("v", false, "output version information and exit")
|
var (
|
||||||
var helpFlag = flag.Bool("h", false, "display this help dialog")
|
versionFlag = flag.Bool("v", false, "output version information and exit")
|
||||||
var filterFlag = flag.String("f", "", "filter containers")
|
helpFlag = flag.Bool("h", false, "display this help dialog")
|
||||||
var activeOnlyFlag = flag.Bool("a", false, "show active containers only")
|
filterFlag = flag.String("f", "", "filter containers")
|
||||||
var sortFieldFlag = flag.String("s", "", "select container sort field")
|
activeOnlyFlag = flag.Bool("a", false, "show active containers only")
|
||||||
var reverseSortFlag = flag.Bool("r", false, "reverse container sort order")
|
sortFieldFlag = flag.String("s", "", "select container sort field")
|
||||||
var invertFlag = flag.Bool("i", false, "invert default colors")
|
reverseSortFlag = flag.Bool("r", false, "reverse container sort order")
|
||||||
var connectorFlag = flag.String("connector", "docker", "container connector to use")
|
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()
|
flag.Parse()
|
||||||
|
|
||||||
if *versionFlag {
|
if *versionFlag {
|
||||||
@@ -56,8 +63,11 @@ func main() {
|
|||||||
// init logger
|
// init logger
|
||||||
log = logging.Init()
|
log = logging.Init()
|
||||||
|
|
||||||
// init global config
|
// init global config and read config file if exists
|
||||||
config.Init()
|
config.Init()
|
||||||
|
if err := config.Read(); err != nil {
|
||||||
|
log.Warningf("reading config: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
// override default config values with command line flags
|
// override default config values with command line flags
|
||||||
if *filterFlag != "" {
|
if *filterFlag != "" {
|
||||||
@@ -77,6 +87,14 @@ func main() {
|
|||||||
config.Toggle("sortReversed")
|
config.Toggle("sortReversed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if *scaleCpu {
|
||||||
|
config.Toggle("scaleCpu")
|
||||||
|
}
|
||||||
|
|
||||||
|
if *defaultShell != "" {
|
||||||
|
config.Update("shell", *defaultShell)
|
||||||
|
}
|
||||||
|
|
||||||
// init ui
|
// init ui
|
||||||
if *invertFlag {
|
if *invertFlag {
|
||||||
InvertColorMap()
|
InvertColorMap()
|
||||||
@@ -85,16 +103,19 @@ func main() {
|
|||||||
if err := ui.Init(); err != nil {
|
if err := ui.Init(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
tm.SetInputMode(tm.InputAlt)
|
||||||
|
|
||||||
defer Shutdown()
|
defer Shutdown()
|
||||||
// init grid, cursor, header
|
// init grid, cursor, header
|
||||||
conn, err := connector.ByName(*connectorFlag)
|
cSuper, err := connector.ByName(*connectorFlag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
cursor = &GridCursor{cSource: conn}
|
cursor = &GridCursor{cSuper: cSuper}
|
||||||
cGrid = compact.NewCompactGrid()
|
cGrid = compact.NewCompactGrid()
|
||||||
header = widgets.NewCTopHeader()
|
header = widgets.NewCTopHeader()
|
||||||
|
status = widgets.NewStatusLine()
|
||||||
|
errView = widgets.NewErrorView()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
exit := Display()
|
exit := Display()
|
||||||
@@ -123,12 +144,13 @@ func validSort(s string) {
|
|||||||
func panicExit() {
|
func panicExit() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
Shutdown()
|
Shutdown()
|
||||||
|
panic(r)
|
||||||
fmt.Printf("error: %s\n", r)
|
fmt.Printf("error: %s\n", r)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var helpMsg = `ctop - container metric viewer
|
var helpMsg = `ctop - interactive container viewer
|
||||||
|
|
||||||
usage: ctop [options]
|
usage: ctop [options]
|
||||||
|
|
||||||
@@ -138,4 +160,6 @@ options:
|
|||||||
func printHelp() {
|
func printHelp() {
|
||||||
fmt.Println(helpMsg)
|
fmt.Println(helpMsg)
|
||||||
flag.PrintDefaults()
|
flag.PrintDefaults()
|
||||||
|
fmt.Printf("\navailable connectors: ")
|
||||||
|
fmt.Println(strings.Join(connector.Enabled(), ", "))
|
||||||
}
|
}
|
||||||
|
|||||||
386
menus.go
386
menus.go
@@ -1,6 +1,10 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/bcicen/ctop/config"
|
"github.com/bcicen/ctop/config"
|
||||||
"github.com/bcicen/ctop/container"
|
"github.com/bcicen/ctop/container"
|
||||||
"github.com/bcicen/ctop/widgets"
|
"github.com/bcicen/ctop/widgets"
|
||||||
@@ -8,17 +12,27 @@ import (
|
|||||||
ui "github.com/gizak/termui"
|
ui "github.com/gizak/termui"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MenuFn executes a menu window, returning the next menu or nil
|
||||||
|
type MenuFn func() MenuFn
|
||||||
|
|
||||||
var helpDialog = []menu.Item{
|
var helpDialog = []menu.Item{
|
||||||
menu.Item{"[a] - toggle display of all containers", ""},
|
{"<enter> - open container menu", ""},
|
||||||
menu.Item{"[f] - filter displayed containers", ""},
|
{"", ""},
|
||||||
menu.Item{"[h] - open this help dialog", ""},
|
{"[a] - toggle display of all containers", ""},
|
||||||
menu.Item{"[H] - toggle ctop header", ""},
|
{"[f] - filter displayed containers", ""},
|
||||||
menu.Item{"[s] - select container sort field", ""},
|
{"[h] - open this help dialog", ""},
|
||||||
menu.Item{"[r] - reverse container sort order", ""},
|
{"[H] - toggle ctop header", ""},
|
||||||
menu.Item{"[q] - exit ctop", ""},
|
{"[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.Clear()
|
||||||
ui.DefaultEvtStream.ResetHandlers()
|
ui.DefaultEvtStream.ResetHandlers()
|
||||||
defer ui.DefaultEvtStream.ResetHandlers()
|
defer ui.DefaultEvtStream.ResetHandlers()
|
||||||
@@ -26,14 +40,18 @@ func HelpMenu() {
|
|||||||
m := menu.NewMenu()
|
m := menu.NewMenu()
|
||||||
m.BorderLabel = "Help"
|
m.BorderLabel = "Help"
|
||||||
m.AddItems(helpDialog...)
|
m.AddItems(helpDialog...)
|
||||||
|
ui.Handle("/sys/wnd/resize", func(e ui.Event) {
|
||||||
|
ui.Clear()
|
||||||
ui.Render(m)
|
ui.Render(m)
|
||||||
|
})
|
||||||
ui.Handle("/sys/kbd/", func(ui.Event) {
|
ui.Handle("/sys/kbd/", func(ui.Event) {
|
||||||
ui.StopLoop()
|
ui.StopLoop()
|
||||||
})
|
})
|
||||||
ui.Loop()
|
ui.Loop()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func FilterMenu() {
|
func FilterMenu() MenuFn {
|
||||||
ui.DefaultEvtStream.ResetHandlers()
|
ui.DefaultEvtStream.ResetHandlers()
|
||||||
defer ui.DefaultEvtStream.ResetHandlers()
|
defer ui.DefaultEvtStream.ResetHandlers()
|
||||||
|
|
||||||
@@ -63,9 +81,10 @@ func FilterMenu() {
|
|||||||
ui.StopLoop()
|
ui.StopLoop()
|
||||||
})
|
})
|
||||||
ui.Loop()
|
ui.Loop()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SortMenu() {
|
func SortMenu() MenuFn {
|
||||||
ui.Clear()
|
ui.Clear()
|
||||||
ui.DefaultEvtStream.ResetHandlers()
|
ui.DefaultEvtStream.ResetHandlers()
|
||||||
defer ui.DefaultEvtStream.ResetHandlers()
|
defer ui.DefaultEvtStream.ResetHandlers()
|
||||||
@@ -87,10 +106,355 @@ func SortMenu() {
|
|||||||
HandleKeys("exit", ui.StopLoop)
|
HandleKeys("exit", ui.StopLoop)
|
||||||
|
|
||||||
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
|
ui.Handle("/sys/kbd/<enter>", func(ui.Event) {
|
||||||
config.Update("sortField", m.SelectedItem().Val)
|
config.Update("sortField", m.SelectedValue())
|
||||||
ui.StopLoop()
|
ui.StopLoop()
|
||||||
})
|
})
|
||||||
|
|
||||||
ui.Render(m)
|
ui.Render(m)
|
||||||
ui.Loop()
|
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) }
|
||||||
|
|||||||
@@ -7,6 +7,29 @@ type Log struct {
|
|||||||
Message string
|
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 {
|
type Metrics struct {
|
||||||
CPUUtil int
|
CPUUtil int
|
||||||
NetTx int64
|
NetTx int64
|
||||||
|
|||||||
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 {
|
func NewCTopHeader() *CTopHeader {
|
||||||
return &CTopHeader{
|
return &CTopHeader{
|
||||||
Time: headerPar(2, timeStr()),
|
Time: headerPar(2, ""),
|
||||||
Count: headerPar(27, "-"),
|
Count: headerPar(24, "-"),
|
||||||
Filter: headerPar(47, ""),
|
Filter: headerPar(40, ""),
|
||||||
bg: headerBg(),
|
bg: headerBg(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ func (i *Input) KeyPress(e ui.Event) {
|
|||||||
if len(i.Data) >= i.MaxLen {
|
if len(i.Data) >= i.MaxLen {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if strings.Index(input_chars, ch) > -1 {
|
if strings.Contains(input_chars, ch) {
|
||||||
i.Data += ch
|
i.Data += ch
|
||||||
i.stream <- i.Data
|
i.stream <- i.Data
|
||||||
ui.Render(i)
|
ui.Render(i)
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ type Padding [2]int // x,y padding
|
|||||||
type Menu struct {
|
type Menu struct {
|
||||||
ui.Block
|
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
|
TextFgColor ui.Attribute
|
||||||
TextBgColor ui.Attribute
|
TextBgColor ui.Attribute
|
||||||
Selectable bool
|
|
||||||
cursorPos int
|
cursorPos int
|
||||||
items Items
|
items Items
|
||||||
padding Padding
|
padding Padding
|
||||||
|
toolTip *ToolTip
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMenu() *Menu {
|
func NewMenu() *Menu {
|
||||||
@@ -41,7 +43,7 @@ func (m *Menu) AddItems(items ...Item) {
|
|||||||
m.refresh()
|
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) {
|
func (m *Menu) DelItem(s string) (success bool) {
|
||||||
for n, i := range m.items {
|
for n, i := range m.items {
|
||||||
if i.Val == s || i.Label == s {
|
if i.Val == s || i.Label == s {
|
||||||
@@ -54,6 +56,11 @@ func (m *Menu) DelItem(s string) (success bool) {
|
|||||||
return success
|
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
|
// Move cursor to an position by Item value or label
|
||||||
func (m *Menu) SetCursor(s string) (success bool) {
|
func (m *Menu) SetCursor(s string) (success bool) {
|
||||||
for n, i := range m.items {
|
for n, i := range m.items {
|
||||||
@@ -65,26 +72,36 @@ func (m *Menu) SetCursor(s string) (success bool) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort menu items(if enabled) and re-calculate window size
|
// SetToolTip sets an optional tooltip string to show at bottom of screen
|
||||||
func (m *Menu) refresh() {
|
func (m *Menu) SetToolTip(lines ...string) {
|
||||||
if m.SortItems {
|
m.toolTip = NewToolTip(lines...)
|
||||||
sort.Sort(m.items)
|
|
||||||
}
|
|
||||||
m.calcSize()
|
|
||||||
ui.Render(m)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Menu) SelectedItem() Item {
|
func (m *Menu) SelectedItem() Item {
|
||||||
return m.items[m.cursorPos]
|
return m.items[m.cursorPos]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Menu) SelectedValue() string {
|
||||||
|
return m.items[m.cursorPos].Val
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Menu) Buffer() ui.Buffer {
|
func (m *Menu) Buffer() ui.Buffer {
|
||||||
var cell ui.Cell
|
var cell ui.Cell
|
||||||
buf := m.Block.Buffer()
|
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 {
|
for n, item := range m.items {
|
||||||
x := m.X + m.padding[0]
|
x := m.X + m.padding[0]
|
||||||
y := m.Y + m.padding[1]
|
|
||||||
for _, ch := range item.Text() {
|
for _, ch := range item.Text() {
|
||||||
// invert bg/fg colors on currently selected row
|
// invert bg/fg colors on currently selected row
|
||||||
if m.Selectable && n == m.cursorPos {
|
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
|
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
|
// Set width and height based on menu items
|
||||||
func (m *Menu) calcSize() {
|
func (m *Menu) calcSize() {
|
||||||
m.Width = 7 // minimum width
|
m.Width = 7 // minimum width
|
||||||
|
|
||||||
items := m.items
|
var height int
|
||||||
for _, i := range m.items {
|
for _, i := range m.items {
|
||||||
s := i.Text()
|
s := i.Text()
|
||||||
if len(s) > m.Width {
|
if len(s) > m.Width {
|
||||||
m.Width = len(s)
|
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.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