Compare commits

...

338 Commits

Author SHA1 Message Date
Bradley Cicenas
8f0c9f5048 v0.7.6 2021-06-11 01:23:43 +00:00
bradley
15708224a7 Merge pull request #262 from judaew/patch-1
Add install variant via MacPorts
2021-06-10 21:19:16 -04:00
bradley
442e70b8df Merge pull request #264 from garsue/use-runewidth-to-increment-textview-x
Use go-runewidth to increment x in TextView
2021-06-10 21:18:33 -04:00
garsue
c58a609349 Update go.sum 2021-05-29 22:01:26 +09:00
garsue
78b038e687 Use runewidth to increment x in TextView 2021-05-29 19:48:02 +09:00
Vadim-Valdis Yudaev
2d937aca08 Add install variant via MacPorts 2021-05-20 17:09:14 +03:00
bradley
0094cba5ea Merge pull request #244 from stokito/fix_243
#243 Fix bug: show ENV variables
2020-12-14 12:29:25 -05:00
bradley
537bb2adfa Merge pull request #247 from stokito/more_columns
More columns
2020-12-14 12:25:17 -05:00
Sergey Ponomarev
491cd85b4d Additional columns 2020-12-11 21:58:37 +02:00
Sergey Ponomarev
9545dfba31 Extract MetaCol 2020-12-11 20:59:35 +02:00
Sergey Ponomarev
cdcb8b6d99 columns.go: remove redundant type 2020-12-11 20:32:22 +02:00
bradley
b562c923b3 Merge pull request #242 from stokito/awesome
Add Alternatives section
2020-12-09 16:10:09 -05:00
Sergey Ponomarev
f2c28c5fb0 #243 Fix bug: show ENV variables
Previously only last env variable is shown
2020-12-09 23:06:14 +02:00
Sergey Ponomarev
ac76b2eac1 Add Alternatives section
This may help users to find more specific tool than ctop
2020-11-30 17:25:40 +02:00
bradley
b32f90fa4a Merge pull request #240 from stokito/name_width
#128 Set column Name to fixed width 30
2020-11-29 09:19:07 -05:00
bradley
bec78c90b5 Merge pull request #238 from stokito/leftovers
Use standard Go functions
2020-11-29 09:06:13 -05:00
Bradley Cicenas
043f4bd3f3 update dockerfile to go 1.15 2020-11-28 20:30:02 +00:00
Sergey Ponomarev
c2401cb33a #128 Set column Name to fixed width 30 2020-11-27 23:12:16 +02:00
Sergey Ponomarev
fa254c652c Columns settings: add hint for re-order columns 2020-11-27 22:34:55 +02:00
Sergey Ponomarev
a59c7aab3c Fix: enable pause [p] only for running or paused container 2020-11-27 22:28:58 +02:00
Sergey Ponomarev
4e44c9d5f7 file.go use filepath.Dir() 2020-11-26 12:08:55 +02:00
Sergey Ponomarev
948e7cc9d0 docker_logs.go use SplitN(2) 2020-11-26 12:08:36 +02:00
Sergey Ponomarev
6c662d91fb docker.go shortName() use TrimPrefix() 2020-11-25 19:17:07 +02:00
Sergey Ponomarev
b4a63f8c60 docker_logs.go: use time.RFC3339Nano constant 2020-11-24 22:19:29 +02:00
Bradley Cicenas
87d135909f rename cpus column 2020-11-23 17:48:56 -05:00
bradley
117c3bc7b5 Merge pull request #234 from stokito/refresh
docker connector: refresh() delete container only if it not found but keep on failures
2020-11-22 19:00:31 -05:00
bradley
b1171f6c3e Merge pull request #235 from stokito/cpu_scale
#207 Replace scaleCpu option with dedicated column CPU Scaled
2020-11-21 12:19:43 -05:00
Sergey Ponomarev
9a41252764 #207 CPU unscaled column changes color according to system total usage
If container uses two cores then CPU column will be always red even if we have dozens of other free cores and CPUS is 1%
2020-11-20 23:24:13 +02:00
Sergey Ponomarev
f377dcaee2 #207 Replace scaleCpu option with dedicated column CPU Scaled
The new column is disabled by default.
2020-11-20 23:08:19 +02:00
Sergey Ponomarev
65e9c6dff6 docker connector: refresh() delete container only if it not found but keep on failures
If inspect() call was failed due to connection problems then container will be removed anyway as like ot wasn't found.
This is probably almost never happens in real life but still some missed logic bug
2020-11-20 19:44:09 +02:00
Sergey Ponomarev
df0d8b7892 #187 "created" action is handled separately in watchEvents() 2020-11-20 19:26:02 +02:00
bradley
2792e72d18 Merge pull request #233 from stokito/exec_shell
Exec shell config
2020-11-20 11:22:59 -05:00
bradley
68d6da5c61 Merge pull request #231 from stokito/log_file
Log to file
2020-11-20 08:30:13 -05:00
Sergey Ponomarev
53a6b36bf5 exec shell: detect default shell
Instead of using configured shell (e.g. bash) we can autodetect default container user's shell and execute it.
This is much safer because not all containers may have installed shell that is configured in ctop.
2020-11-20 10:42:52 +02:00
Sergey Ponomarev
5ec02f760e exec shell: remove shell config or option
The option is never worked and can't properly work because almost all containers anyway using Ash/Dash from /bin/sh
2020-11-20 10:39:28 +02:00
Sergey Ponomarev
83a422933a exec shell: on error show a status message instead of fatal exit 2020-11-20 10:33:44 +02:00
Sergey Ponomarev
7679d4a7fd exec shell: fix shell config
Currently each time when ctop started it overwrites "shell" config with default value of program argument.
This means that in fact default shell config is never worked at all.
2020-11-20 10:31:00 +02:00
Sergey Ponomarev
e34afceb5f logging: log to file
New env var CTOP_DEBUG_FILE to specify a path to log file
2020-11-19 20:11:46 +02:00
bradley
ddfff03c05 Merge pull request #229 from stokito/events
Improve docker events handling
2020-11-19 11:27:09 -05:00
Sergey Ponomarev
29d90cfdd9 logging: start server after log is configured
Without this "logging server started" is written to stderr
2020-11-19 11:21:27 +02:00
Sergey Ponomarev
ba126e6e7c events handling: early skipping of extremely frequent exec_* events
The exec_create, exec_start, exec_die and other events are generated by health checks
2020-11-18 23:04:29 +02:00
Sergey Ponomarev
009201ed0c actionToStatus: catch more generic die instead of kill 2020-11-18 23:04:29 +02:00
Sergey Ponomarev
2c07cab59c logging: skip timer events e.g. /timer/1s
Each second we receive the timer event which makes little sense but log is bloated with the event.
To make logs more readable we can disable logging of this event
2020-11-18 23:04:29 +02:00
bradley
fd06992236 Merge pull request #230 from stokito/skip_timer_logging
logging: skip timer events e.g. /timer/1s
2020-11-18 09:40:33 -05:00
Sergey Ponomarev
e64edbdc36 logging: skip timer events e.g. /timer/1s
Each second we receive the timer event which makes little sense but log is bloated with the event.
To make logs more readable we can disable logging of this event
2020-11-18 11:25:20 +02:00
Sergey Ponomarev
4c280cee56 Improve docker events handling: separate channel for status updates
Instead of calling a heavy inspect api call we can easily changing status just by knowing an action.
Let's do this in a separate channel decouple UI update from basic event loop.
2020-11-17 22:00:03 +02:00
Sergey Ponomarev
b65e970a83 Improve docker events handling
Firstly check health updates because it will be a lot of them especially when a container is unstable.
Clearly handle create event to create a container even if it wasn't started.
Instead of die event handle more precise events like oom, kill and only then trigger refresh.
Lookup the state from a map.
2020-11-17 21:50:25 +02:00
Sergey Ponomarev
957cabba2d docker.go: watchEvents() optimize actionName extraction
Split(e.Action, ":") creates and array but we can avoid this.
2020-11-17 12:06:34 +02:00
Sergey Ponomarev
5aacdc3772 docker.go: logging of events
Add log.IsEnabledFor(logging.DEBUG) guard to avoid unnecessary memory allocations. Debug level is usually disabled.
Inline "destroy"
2020-11-17 11:42:45 +02:00
Bradley Cicenas
99d9aeec98 set running = false when mock,runc collectors stopped 2020-11-13 21:43:11 +00:00
bradley
44600fca45 Merge pull request #201 from vcmkrtchyan/master
Fix freeze when container is started/stopped multiple times
2020-11-13 16:42:16 -05:00
bradley
9aaba5dfca Merge pull request #223 from stokito/trim
file.go: remove duplicated trim
2020-11-13 15:40:01 -05:00
Bradley Cicenas
0bd8efe800 fix makefile release 2020-11-13 20:35:40 +00:00
Sergey Ponomarev
3a29c94833 Use percent() function to calc CPU usage 2020-11-13 11:02:14 +02:00
Sergey Ponomarev
a22d99fefb file.go: remove duplicated trim
The s var was already trimmed on line 67
2020-11-12 21:23:18 +02:00
Bradley Cicenas
c971d26d42 v0.7.5 2020-11-06 16:59:24 +00:00
Bradley Cicenas
de380ff810 add arch section to readme 2020-11-06 16:56:17 +00:00
bradley
e7e2478468 Merge pull request #222 from azlux/master
Readme : fix link
2020-11-05 13:44:35 -05:00
azlux
42c80c2395 fix link 2020-11-05 19:24:50 +01:00
bradley
d22bbc3420 Merge pull request #221 from azlux/master
Add apt azlux's repository
2020-11-05 13:16:22 -05:00
azlux
bdfb98265d Add apt azlux's repository 2020-11-05 10:54:10 +01:00
Bradley Cicenas
af1908fb27 update make release to use ghub cli 2020-11-03 07:55:39 -05:00
bradley
73a976c6fe Merge pull request #220 from stokito/single_view_trunc_id
Single Container View: truncate ID
2020-11-03 07:55:00 -05:00
Sergey Ponomarev
2bcfc365f7 Single Container View: truncate ID
Truncation Id inside of NewSingle() doesn't work.
It even doesn't have any effect.
Instead truncate the ID inside of NewMeta() call
2020-11-03 14:11:58 +02:00
bradley
ecc7bf4081 Merge pull request #219 from bcicen/text-col-setter
add textcol setter, static CID column width
2020-10-31 10:53:42 -04:00
Bradley Cicenas
68e4c32c1b add textcol setter, static CID column width 2020-10-31 14:52:30 +00:00
bradley
a63f05b430 Merge pull request #211 from stokito/truncate_column
text.go: to do not manually truncate id and name columns
2020-10-31 10:40:52 -04:00
Bradley Cicenas
426dd2c985 update dockerclient -> v1.6.6 2020-10-26 19:30:33 +00:00
bradley
0fb627a529 Merge pull request #214 from whalehub/static-binaries
Makefile: Add CGO_ENABLED=0 to generate statically linked binaries
2020-10-26 15:11:02 -04:00
Aaron
c8f74a47a1 Makefile: Add CGO_ENABLED=0 to generate statically linked binaries
Signed-off-by: Aaron <admin@datahoarder.dev>
2020-10-26 16:14:57 +01:00
Sergey Ponomarev
41c04fefa2 text.go: to do not manually truncate id and name columns
This truncation is already handled by termui
2020-10-26 15:43:55 +02:00
bradley
5b2d180f60 Merge pull request #210 from bcicen/fix-config-encoding
Fix config encoding
2020-10-26 09:04:07 -04:00
Bradley Cicenas
2fdbb91f87 ensure existing config file is removed prior to writing 2020-10-26 13:04:00 +00:00
Bradley Cicenas
c0703db094 add health check to mock connector 2020-10-26 12:02:31 +00:00
Bradley Cicenas
53ec5c911a return static error on unimplemented manager actions 2020-10-26 11:38:17 +00:00
Bradley Cicenas
c5038e2edd aggregate io r/w across all reported stats volumes 2020-10-25 17:42:20 +00:00
Bradley Cicenas
e1a52a314d v0.7.4, update docs 2020-10-25 16:55:44 +00:00
Bradley Cicenas
bbecbc66b9 fix byte format for compact view (short) 2020-10-25 16:02:31 +00:00
Bradley Cicenas
192d3eaa7a Merge branch '0.7.4-dev' into master 2020-10-25 11:41:49 -04:00
Bradley Cicenas
d34de844e0 add column config key to help menu 2020-10-25 10:31:51 -04:00
Bradley Cicenas
a8e235beca commit missing compact column, row files 2020-10-25 10:31:51 -04:00
Bradley Cicenas
ed194e8c04 update to go 1.13 2020-10-25 10:31:51 -04:00
Bradley Cicenas
09566a4043 add optional tooltip to menu widget 2020-10-25 10:31:51 -04:00
Bradley Cicenas
f11a705b8b add padding to column menu 2020-10-25 10:31:51 -04:00
Bradley Cicenas
6fe6e7c316 truncate id in compact widget 2020-10-25 10:31:51 -04:00
Bradley Cicenas
9aa104fbc6 handle single kv pair given to NewMeta 2020-10-25 10:31:51 -04:00
Bradley Cicenas
7c6b5c54dc init column config menu 2020-10-25 10:31:51 -04:00
Bradley Cicenas
ffb96f4e90 handle empty entries in column config 2020-10-25 10:31:51 -04:00
Bradley Cicenas
fc9bd9e5ca refactor column config 2020-10-25 10:31:51 -04:00
Bradley Cicenas
c7a8bfa26f integrate widget order, toggling into global config and compact grid 2020-10-25 10:31:51 -04:00
Bradley Cicenas
6b79e5a370 remove unused static col width 2020-10-25 10:31:51 -04:00
Bradley Cicenas
54fc5ac5c6 Revert "shows total memory usage"
This reverts commit 1271ce96e8.
2020-10-25 10:31:51 -04:00
Bradley Cicenas
eb8237cbb1 continuing compact widget refactor 2020-10-25 10:31:51 -04:00
Bradley Cicenas
60875b179c initial refactor of all column widgets to standard interface 2020-10-25 10:31:19 -04:00
Marcos Diez
15c5c31726 shows total memory usage 2020-10-25 10:23:31 -04:00
Bradley Cicenas
ea5968edce update sig 2020-10-25 10:23:30 -04:00
Bradley Cicenas
c0db41ebcb Revert "shows total memory usage"
This reverts commit 1271ce96e8.
2020-10-25 10:23:30 -04:00
Bradley Cicenas
dc14c79edf add run-dev to makefile 2020-10-25 10:23:30 -04:00
Bradley Cicenas
0ca5235ae5 continuing compact widget refactor 2020-10-25 10:23:30 -04:00
Bradley Cicenas
8427b0c81d initial refactor of all column widgets to standard interface 2020-10-25 10:23:30 -04:00
Marcos Diez
9bcf2c2c7a shows total memory usage 2020-10-25 10:23:30 -04:00
Bradley Cicenas
03a0da3230 update go version, deps for runc v1.0.0-rc92 2020-10-25 14:22:47 +00:00
Bradley Cicenas
4d7d69d4cf refactor byte unit formatting, add unit-specific display precision 2020-10-25 14:08:06 +00:00
bradley
fae9deb1d9 Merge pull request #203 from kerolloz/patch-1
enhance README.md
2020-08-28 07:29:46 -04:00
Kerollos Magdy
8027b990f8 enhance README.md 2020-07-30 16:18:24 +02:00
Vahe Mkrtchyan
4a0e80ffdf Fix freeze when container is started/stopped multiple times 2020-07-23 15:23:19 +04:00
Bradley Cicenas
c446fb0e11 Merge branch 'master' of github.com:bcicen/ctop into 1.8-dev 2020-01-03 13:50:46 -05:00
Bradley Cicenas
4741b276e4 v0.7.3 2020-01-03 18:37:30 +00:00
Bradley Cicenas
d60b16aad1 add column config key to help menu 2020-01-03 13:21:57 +00:00
Bradley Cicenas
f704898212 commit missing compact column, row files 2020-01-03 13:20:31 +00:00
Bradley Cicenas
1523cc80ca update to go 1.13 2020-01-03 13:18:01 +00:00
Bradley Cicenas
b16561dccb Merge branch 'refactor-widgets' 2020-01-03 08:04:51 -05:00
Bradley Cicenas
bf3b89a010 add optional tooltip to menu widget 2020-01-03 12:53:25 +00:00
Bradley Cicenas
5585a22962 add padding to column menu 2020-01-03 12:25:54 +00:00
Bradley Cicenas
ca5d40b7cc truncate id in compact widget 2020-01-03 12:07:54 +00:00
Bradley Cicenas
50d1c29d57 handle single kv pair given to NewMeta 2020-01-03 12:07:21 +00:00
Bradley Cicenas
22a5607012 init column config menu 2020-01-02 23:02:53 +00:00
Bradley Cicenas
6e60fc905e handle empty entries in column config 2020-01-02 19:29:20 +00:00
Bradley Cicenas
118b89240d refactor column config 2020-01-02 19:28:51 +00:00
Bradley Cicenas
ee25f80a9c integrate widget order, toggling into global config and compact grid 2020-01-02 14:00:55 +00:00
Bradley Cicenas
416eb5c363 update sig 2019-12-30 08:39:03 -05:00
Bradley Cicenas
746da760fb Revert "shows total memory usage"
This reverts commit 1271ce96e8.
2019-12-30 08:39:03 -05:00
Bradley Cicenas
cc6f706c4b add run-dev to makefile 2019-12-30 08:39:03 -05:00
Bradley Cicenas
1ca40bb7e1 continuing compact widget refactor 2019-12-30 08:39:03 -05:00
Bradley Cicenas
918ccdbe39 initial refactor of all column widgets to standard interface 2019-12-30 08:39:03 -05:00
Marcos Diez
8fcd14e097 shows total memory usage 2019-12-30 08:39:03 -05:00
bradley
9dd12103fc Merge pull request #186 from alexislefebvre/patch-1
Format options and keybindings as code
2019-12-30 08:31:49 -05:00
Alexis Lefebvre
c38942c7ed Format options and keybindings as code 2019-11-21 17:34:53 +01:00
Bradley Cicenas
d34b9c2bf6 remove unused static col width 2019-11-11 22:04:25 +00:00
Bradley Cicenas
a60861437f Merge branch 'refactor-widgets' 2019-11-06 07:33:42 -05:00
Bradley Cicenas
4b391e900c use raw log stream in docker log collector 2019-11-06 07:32:29 -05:00
Bradley Cicenas
4460162380 go 1.13 2019-11-06 12:32:03 +00:00
Bradley Cicenas
d56cc9475a update sig 2019-11-06 12:31:57 +00:00
bradley
4584cf34f5 Merge pull request #178 from rvolosatovs/feature/go-sum
Add go.sum
2019-10-03 07:35:26 -04:00
Roman Volosatovs
1ce07448ce go.sum: Add 2019-09-10 20:22:25 +02:00
Bradley Cicenas
c8e896e371 Revert "shows total memory usage"
This reverts commit 1271ce96e8.
2019-07-03 11:27:17 +00:00
Bradley Cicenas
db2c832bd7 add run-dev to makefile 2019-07-05 19:09:30 -04:00
Bradley Cicenas
7fdcd7bbf1 continuing compact widget refactor 2019-07-05 19:09:30 -04:00
Bradley Cicenas
923edb967e initial refactor of all column widgets to standard interface 2019-07-05 19:08:16 -04:00
Marcos Diez
1271ce96e8 shows total memory usage 2019-07-05 19:07:13 -04:00
Bradley Cicenas
d8c7dd4c5c move image to go 1.12 2019-06-29 15:15:13 -04:00
Bradley Cicenas
b7d81485f9 update dockerclient, runc deps 2019-06-29 15:15:13 -04:00
bradley
8946c4b03b Merge pull request #169 from captn3m0/patch-1
Switches to a read-only socket mount
2019-06-29 14:32:08 -04:00
Bradley Cicenas
331f50f03e update status doc image 2019-06-22 18:44:07 +00:00
Bradley Cicenas
4c4f041b40 improve health check visibility 2019-06-22 18:42:48 +00:00
Bradley Cicenas
c8ac331652 fix timer 2019-05-22 17:39:37 +00:00
Bradley Cicenas
0a5a4c9062 add multi-line scrolling support, timestamps to error view 2019-05-22 17:38:01 +00:00
Bradley Cicenas
98fcfe8b6f refactor connectors for retry logic, add error view 2019-05-22 16:58:55 +00:00
bradley
42f095cd85 Merge pull request #170 from fr05t1k/exec
Enable cursor
2019-05-13 12:24:41 -04:00
Stanislav Pavlovichev
73986d2732 Enable cursor 2019-05-13 15:47:22 +03:00
Bradley Cicenas
c1d4615cc0 Merge branch 'fr05t1k-exec' 2019-05-12 16:26:51 -04:00
Bradley Cicenas
d187e8c623 drop potentially empty initial frames during exec attach 2019-05-12 20:23:54 +00:00
Bradley Cicenas
b8c38d09ef add exec shortcut key to container menu 2019-05-12 20:23:29 +00:00
Bradley Cicenas
d7384db373 Merge branch 'exec' of https://github.com/fr05t1k/ctop into fr05t1k-exec 2019-05-12 15:31:33 -04:00
Nemo
1b441db189 Switches to a read-only socket mount 2019-04-17 00:43:15 +05:30
bradley
0479d42e31 Merge pull request #168 from CodeLingoBot/rewrite
Fix function comments based on best practices from Effective Go
2019-03-07 09:41:02 +05:30
CodeLingo Bot
b401e7b17e Fix function comments based on best practices from Effective Go
Signed-off-by: CodeLingo Bot <bot@codelingo.io>
2019-03-07 02:33:29 +00:00
Bradley Cicenas
9592de82a0 add keyboard shortcuts to container menu 2019-01-24 14:06:40 +00:00
bradley
29fa8cf3e7 Update status.md 2019-01-24 08:02:52 -05:00
bradley
c49939f965 Update status.md 2019-01-24 08:02:13 -05:00
bradley
2f7bc2a172 Update status.md 2019-01-24 08:00:51 -05:00
Bradley Cicenas
7b4d4db049 add status indicator doc 2019-01-24 12:50:59 +00:00
Bradley Cicenas
70bd2ae3a3 v0.7.2 2019-01-24 11:50:49 +00:00
Bradley Cicenas
665e8fdd06 move to go module 2018-12-01 17:50:47 +00:00
Stanislav Pavlovichev
101ddad692 Fixed a problem with rendering 2018-10-28 13:43:24 +02:00
Stanislav Pavlovichev
ca35ef2aab Unnecessary loop stopping 2018-10-28 12:07:43 +02:00
Stanislav Pavlovichev
d59c91a461 Do not allow to close /dev/stdin 2018-10-26 17:08:33 +03:00
bradley
a39b7a3a3e Merge pull request #152 from barthr/master
Refactoring improvements based on linting issues
2018-10-26 09:00:02 -05:00
bartfokker
77f5e6b735 remove ignore of variable (unneeded when only index is needed) 2018-10-25 22:25:36 +02:00
bartfokker
3c83b7576b refactor string on multiple places to constant 2018-10-25 22:23:44 +02:00
bartfokker
8a0bd3cf8a remove unneeded cast 2018-10-25 22:22:28 +02:00
bartfokker
78caad2dbd depend on io.WriteCloser instead of net.Conn 2018-10-25 22:22:04 +02:00
bartfokker
8d8f1e72eb rename ConfigFile to File because config.ConfigFile stutters. Instead it's config.File 2018-10-25 22:21:08 +02:00
bartfokker
93556a1754 replace += with ++ 2018-10-25 22:17:53 +02:00
bartfokker
4d247f5272 replace unkeyed fiels with keyed fields when instantiating log struct 2018-10-25 22:17:05 +02:00
bartfokker
db3d7e8927 change strings.Index for strings.Contains 2018-10-25 22:14:00 +02:00
bartfokker
efef345665 remove unneeded fmt.Sprintf 2018-10-25 22:13:04 +02:00
bartfokker
f158fa742f simplify append operation by omitting loop 2018-10-25 22:12:46 +02:00
bartfokker
4d48245d7d improve boolean logic 2018-10-25 22:12:17 +02:00
bartfokker
6bee1b7f31 remove unneeded select for simple channel receive 2018-10-25 22:11:17 +02:00
bartfokker
7118e45f3a add vendor directory to gitignore 2018-10-25 22:04:40 +02:00
Stanislav Pavlovichev
a26fc9169c Ability to change Shell 2018-10-25 21:58:33 +03:00
Stanislav Pavlovichev
967a87a65f Exec using API 2018-10-13 08:33:53 +03:00
Stanislav Pavlovichev
e68f7ba96a fix: handlers used to work after "exec sh" command
feature: hot key for "exec sh"
2018-10-12 10:03:27 +03:00
bradley
3405d19be8 Merge pull request #147 from serg-bloim/env-var
Display environment variables on single view page
2018-10-10 21:45:08 +08:00
Stanislav Pavlovichev
f27de1c29e Exec Sh Feature 2018-10-07 16:46:32 +03:00
Serhii Bilonozhko
9a185b2388 env-var 2018-10-05 17:35:22 -04:00
Bradley Cicenas
caf6fc63c1 add config toggle for full-row cursor 2018-09-17 01:33:52 +00:00
Bradley Cicenas
cf352f7c8a implement full-row cursor highlighting 2018-09-17 01:24:06 +00:00
bradley
ac5bed210f Merge pull request #143 from jphautin/add_networks_ips
add IP of networks in single view mode
2018-09-15 10:43:22 +09:00
Jean-Philippe
a72d43526f add IP of networks in single view mode 2018-09-06 21:01:16 +02:00
bradley
9eb2457aa4 Merge pull request #132 from xiechengsheng/fix-120
add support for alternative navigation
2018-06-30 08:31:44 +02:00
Bradley Cicenas
b83402b886 add TERM env var to Dockerfile 2018-06-28 11:19:07 +00:00
xiechengsheng
078564bd38 add support for alternative navigation
Signed-off-by: xiechengsheng <XIE1995@whut.edu.cn>
2018-06-28 16:40:26 +08:00
bradley
a2c08d312e Merge pull request #131 from xiechengsheng/add-pause-unpause
Feature: add more commands in container manager menu
2018-06-28 10:25:45 +02:00
xiechengsheng
f7a3d38d6b add more commands in container manager menu
Signed-off-by: xiechengsheng <XIE1995@whut.edu.cn>
2018-06-22 15:41:16 +08:00
Bradley Cicenas
a3b8585697 add requirement check to install script 2018-06-13 09:20:05 +00:00
bradley
2e526e9b86 Merge pull request #130 from felipeconti/master
Installing on /usr/local/bin as root
2018-06-13 11:04:34 +02:00
Felipe B. Conti
541fe70b78 Remove unnecessary treatment 2018-06-10 14:12:07 -03:00
Felipe Conti
c786b697bf Add function command_exists 2018-06-10 14:04:34 -03:00
Felipe Conti
aa6c00b083 Treatment to use root 2018-06-10 14:02:42 -03:00
Bradley Cicenas
17855e3d8e add container name to log view title 2018-05-10 09:53:59 +00:00
Bradley Cicenas
842809bef5 enable termbox alt input 2018-05-10 09:44:57 +00:00
Bradley Cicenas
4e567ee007 update README 2018-03-09 05:37:36 +00:00
Bradley Cicenas
56700e120b add go-winio dep 2018-03-09 05:28:11 +00:00
Bradley Cicenas
5261444265 v0.7.1 2018-03-09 05:19:34 +00:00
Bradley Cicenas
b3aa291182 update termbox-go dependency 2018-03-09 05:15:55 +00:00
Bradley Cicenas
051b474bf0 dep updates 2018-02-23 13:57:59 +09:30
Bradley Cicenas
fac6632459 build image from go 1.10 2018-02-22 14:58:21 +09:30
Bradley Cicenas
1c7cf98e58 handle window resize in help dialog 2018-02-02 15:21:33 +00:00
Bradley Cicenas
44a54e070d combine conditionals 2018-02-01 16:55:23 +00:00
Bradley Cicenas
10b9a6c013 include windows build 2018-01-31 17:05:54 +00:00
Bradley Cicenas
a3b67e4607 add available connectors to help dialog 2018-01-30 07:54:13 -03:00
Bradley Cicenas
ac1ce18143 refactor enabled connectors 2018-01-29 12:47:10 +00:00
bradley
01a305d326 Update README.md
escape brackets
2018-01-11 15:58:37 -03:00
Bradley Cicenas
233259be40 v0.7 2018-01-11 15:32:34 -03:00
Bradley Cicenas
107def9ccc add status err messages to start/stop/remove 2018-01-11 15:27:30 -03:00
Bradley Cicenas
d46ce783c2 add statusline widget, status messages to logging 2018-01-11 15:19:01 -03:00
Bradley Cicenas
d743472b16 add support for config file, keybinding for exporting active config 2018-01-11 13:15:18 -03:00
Bradley Cicenas
d785b263f4 add scale-cpu switch 2018-01-11 12:19:00 -03:00
Bradley Cicenas
6d37a4e333 enable remove action for "created" state containers 2018-01-11 11:40:21 -03:00
Bradley Cicenas
eb49e51ffb prevent recursive/nested running menus, handle all within main Display() loop 2018-01-11 11:23:28 -03:00
Bradley Cicenas
734d4bfc0c add optional subtext to menu widget 2018-01-11 10:56:00 -03:00
Bradley Cicenas
0e75bbda58 add confirmation dialog for container management actions 2018-01-11 10:26:58 -03:00
Bradley Cicenas
8b5eb21ac3 add logs to container menu 2018-01-10 18:10:11 -03:00
Bradley Cicenas
c958e4c34e move keybinding for single view, add single view to container menu 2018-01-10 15:45:14 -03:00
Bradley Cicenas
ab48d830d1 import formatting 2017-12-13 09:20:14 +08:00
bradley
915579c0a4 Merge pull request #110 from HotelsDotCom/master
added option to toggle log timestamp in the log view, closes #107
2017-12-13 09:16:33 +08:00
Peter Reisinger
0a6e6f02a4 added option to toggle log timestamp in the log view, closes #107 2017-12-08 11:35:04 +00:00
Bradley Cicenas
a135f45844 add simple output fn to installer script 2017-12-08 09:37:54 +08:00
Bradley Cicenas
b39b91774d add checksum validation to installer script 2017-12-05 17:50:00 +08:00
Bradley Cicenas
2275248813 init simple installer script 2017-12-02 20:49:37 +00:00
bradley
75f4a91f11 Merge pull request #108 from HotelsDotCom/master
line wrapping in log view, closes #106
2017-12-01 21:47:05 -05:00
Peter Reisinger
d0b5c6c854 added test for text view split line 2017-12-01 15:51:55 +00:00
Peter Reisinger
bd8940ae0a line wrapping in log view, closes #106 2017-12-01 15:29:11 +00:00
Bradley Cicenas
1be64f0d11 define flag vars in block 2017-11-29 03:03:46 +00:00
Bradley Cicenas
1c8f4b3a35 add logging for log reader start/stop 2017-11-28 09:38:32 -05:00
Bradley Cicenas
3e5176a79c add NewDockerLogs constructor method 2017-11-28 09:36:28 -05:00
Bradley Cicenas
53c0f2a9df add resize handling to log view 2017-11-28 08:55:29 -05:00
Bradley Cicenas
28389aa38c gofmt 2017-11-28 08:40:43 -05:00
bradley
fb5c825cf6 Merge pull request #105 from HotelsDotCom/master
Added 'view logs binding'
2017-11-26 12:41:57 -05:00
Peter Reisinger
a0e0da1da9 Added 'view logs binding' 2017-11-25 18:30:50 +00:00
Bradley Cicenas
a826859202 gofmt 2017-11-22 09:27:38 -05:00
Bradley Cicenas
71b4a1de94 add runc manager placeholder 2017-11-22 09:26:01 -05:00
bradley
93a2e4b1ca Merge pull request #104 from HotelsDotCom/master
added container menu - closes #28
2017-11-22 08:22:37 -06:00
Peter Reisinger
436266b1a4 added container menu closes #28 2017-11-20 11:09:36 +00:00
Bradley Cicenas
19427e33a0 add checksum verify to make release 2017-11-19 15:06:29 +00:00
Bradley Cicenas
48d683be77 add checksum generation to makefile 2017-11-18 07:51:15 +00:00
bradley
e1ec264345 Merge pull request #90 from bcicen/godep
Move from Glide -> Dep
2017-09-03 11:10:59 +09:00
Bradley Cicenas
9aad2efdb0 update build docs with dep link 2017-09-03 10:53:01 +09:00
Bradley Cicenas
f6595a02c4 replace glide with godep 2017-09-03 10:33:14 +09:00
Bradley Cicenas
92ca9bf7eb remove textcol coloring 2017-08-28 10:48:13 +09:00
Bradley Cicenas
05242a83f0 refactor status widget, include health indicator 2017-08-28 10:45:14 +09:00
Bradley Cicenas
add44c0f18 add common status colors to global theme 2017-08-28 08:46:01 +09:00
Bradley Cicenas
a1ebf3f90e remove duplicate inspect, trigger refresh on "health_status" events 2017-08-28 08:29:59 +09:00
Alexandr Kozlenkov
626d50d3e9 Added health row to Info Single mode.
Change color status logic.

Highlight health status for Name column:
- starting - yellow
- healthy - green
- unhealthy - magenta

reformat

Fixed misprint

Removed unused colors of state widget.

Moved changes to another branch

Removed unused colors of state widget.

Remove swarm changes from master

Remove swarm changes from master

Remove swarm changes from master
2017-08-28 07:55:43 +09:00
bradley
eaa7ad85f8 Merge pull request #87 from mavimo/patch-1
Update version in readme
2017-08-25 14:22:04 +09:00
Marco Vito Moscaritolo
be9be0b2d1 Update version in readme
SSIA
2017-08-24 12:27:15 +02:00
bradley
f196999c67 Merge pull request #86 from kentaost/runc-root-description
update RUNC_ROOT description to adjust to runC man
2017-08-22 15:22:19 +09:00
Kenta Tada
e674ec4f33 update RUNC_ROOT description to adjust to runC man 2017-08-21 21:36:45 +09:00
Bradley Cicenas
f23805550f remove deprecated -e option 2017-08-19 14:33:56 +09:00
Bradley Cicenas
55a356bbec omit cache from memory usage stat 2017-08-11 16:44:52 +02:00
Bradley Cicenas
954aaeb06b rename Memory widget 2017-08-09 14:05:45 +02:00
Bradley Cicenas
27e272c58f rename expanded -> single view 2017-08-05 13:28:20 +02:00
Bradley Cicenas
3ed9912bcb update circleci 2017-06-29 14:02:52 +00:00
Bradley Cicenas
e0f6563a39 move circleci config to 2.0 2017-06-29 14:02:52 +00:00
Bradley Cicenas
caa64724d0 fix release dirname 2017-06-29 14:02:52 +00:00
Bradley Cicenas
a2011b8bc7 v0.6.1 2017-06-29 14:02:52 +00:00
Bradley Cicenas
40fd9e935a combine image build steps 2017-06-29 14:02:52 +00:00
Bradley Cicenas
b88c143914 add offset sanity check to CompactGrid Align() 2017-07-07 15:43:03 +03:00
Bradley Cicenas
0a05007c4e skip offset updates in page scroll if no pages 2017-07-07 15:38:02 +03:00
Bradley Cicenas
c47ba3f804 add pgCount() method to GridCursor 2017-07-07 15:28:26 +03:00
Bradley Cicenas
79a3f361a7 add container log struct to models, collectors 2017-07-04 12:32:25 +00:00
Bradley Cicenas
65399a37e5 add log panel to expanded widgets 2017-06-29 14:02:52 +00:00
Bradley Cicenas
25a3fcf731 add runtimestats, stack logging to debug 2017-06-28 09:12:24 -03:00
Bradley Cicenas
17e2c2df8e add LogCollector interface, docker, mock log collectors 2017-06-27 14:18:17 -03:00
Bradley Cicenas
240345d527 add StreamLogs() to collector interface 2017-06-26 15:35:57 +00:00
Bradley Cicenas
2d284d9277 rename metrics subpackage 2017-06-26 15:35:57 +00:00
Bradley Cicenas
bfa5c5944f rename 2017-06-26 15:35:57 +00:00
Bradley Cicenas
e1051cd40f use underscore-prefixed build dir in makefile 2017-06-24 08:04:56 -03:00
Bradley Cicenas
13029cc7fe add go runtime to version output 2017-06-19 12:23:39 +00:00
Bradley Cicenas
58d9e4e194 reverse host and container port in metadata 2017-06-18 17:17:56 -03:00
Bradley Cicenas
4de7036e2f fix release url 2017-06-14 15:04:47 -03:00
Bradley Cicenas
617b1b2863 omit runc connector from darwin build 2017-06-14 10:11:40 -03:00
bradley
e59a360b60 Create connectors.md 2017-06-14 09:32:48 -03:00
bradley
91cd53a878 change option header sizing 2017-06-14 09:32:26 -03:00
Bradley Cicenas
c6f2c7b617 add badges back 2017-06-14 09:30:03 -03:00
Bradley Cicenas
568bfb2513 v0.6 doc updates 2017-06-14 09:17:44 -03:00
Bradley Cicenas
66ff8ad7ec update potentially conflicting variable name 2017-06-13 17:35:00 -03:00
Bradley Cicenas
400d9471b6 add pidcount,io to mock collector 2017-06-13 17:25:58 -03:00
Bradley Cicenas
288380ca8d remove arbitrary branch deploy commands 2017-06-12 11:25:02 -03:00
Bradley Cicenas
429d5b9101 v0.6.0 2017-06-12 11:16:03 -03:00
Bradley Cicenas
1be452d7c0 refactor collectors into subpackage 2017-06-12 11:12:03 -03:00
Bradley Cicenas
671c944272 disable timed display refresh while scrolling 2017-06-12 10:52:45 -03:00
Bradley Cicenas
a48a9031cc move container sort to struct method 2017-06-12 10:40:52 -03:00
Bradley Cicenas
aff6943d07 add runc connector doc 2017-06-10 12:25:18 -03:00
Bradley Cicenas
f56ff96b88 Revert "remove build-time container in make image"
This reverts commit b49e174483.
2017-06-10 12:15:42 -03:00
Bradley Cicenas
b5361c2a28 panic on missing runc root 2017-06-10 11:44:12 -03:00
Bradley Cicenas
e71b6cacce prevent unlock until container sort complete 2017-06-10 11:09:21 -03:00
Bradley Cicenas
8a1297d3c5 add created meta field to expanded view 2017-06-10 10:46:54 -03:00
Bradley Cicenas
bdea7d5853 remove containers from connector map on destroyed state 2017-06-10 10:46:16 -03:00
Bradley Cicenas
389dee0f3c add percent helper method to metrics 2017-06-10 10:00:54 -03:00
Bradley Cicenas
b49e174483 remove build-time container in make image 2017-06-10 09:37:36 -03:00
Bradley Cicenas
53b612ab07 add additional logging messages 2017-06-10 09:36:34 -03:00
Bradley Cicenas
446708e456 add default runc root path 2017-06-09 18:35:28 -03:00
Bradley Cicenas
1233ff0ead add arbitrary branch image pushes to circle ci 2017-06-09 18:18:48 -03:00
Bradley Cicenas
af3f1e2a85 update glide deps 2017-06-09 18:11:59 -03:00
Bradley Cicenas
4dbc5653ff update build ldflags to permit multiple runc dep versions 2017-06-09 15:18:11 -03:00
Bradley Cicenas
e8d9f3327c runc connector optimizations 2017-06-09 14:56:39 -03:00
Bradley Cicenas
d372043a17 add --connector switch, validation 2017-06-09 14:35:29 -03:00
Bradley Cicenas
eeac65da8c add sys proc methods to metrics 2017-06-09 14:15:12 -03:00
Bradley Cicenas
c1780ae30a add byte formatting for tb 2017-06-09 14:14:27 -03:00
Bradley Cicenas
fb39d69fa7 add runc metric collector 2017-06-09 13:07:25 -03:00
Bradley Cicenas
6392d63ff8 prevent panic messages from being hidden due to ui.Init() race condition 2017-06-08 17:19:34 -03:00
Bradley Cicenas
b009a260a4 initial runc connector implementation 2017-06-08 15:33:34 -03:00
Bradley Cicenas
44379cd9fd rename connectors 2017-06-08 12:01:08 -03:00
Bradley Cicenas
b85ca680f0 restructure container,connectors in subpackage 2017-06-08 11:51:02 -03:00
Bradley Cicenas
8fb7a8988f include <escape> in exit keygroup 2017-06-08 09:47:30 -03:00
Bradley Cicenas
6d097c2085 add quickstart section to debug doc 2017-06-08 09:33:07 -03:00
Bradley Cicenas
f9d68d688d add tcp logging section to debug doc 2017-05-31 10:55:31 -04:00
Bradley Cicenas
bc08b85191 add option to log debug messages to unix or tcp socket 2017-05-31 10:45:48 -04:00
Bradley Cicenas
f3d26e038d use current directory as default logging socket path 2017-05-31 10:21:32 -04:00
Bradley Cicenas
b4e1fbf290 add release, homebrew badges to README 2017-05-28 17:03:19 -04:00
Bradley Cicenas
58d5fba945 update port formatting for multi-line display 2017-05-15 11:54:35 +01:00
Bradley Cicenas
c76036a6f2 allow multi-line metadata in expanded view 2017-05-15 11:37:27 +01:00
bradley
6a8848d1e2 Merge pull request #65 from kenan-rhoton/ports
Add Ports information to the expanded view
2017-05-15 11:35:20 +01:00
Kenan Rhoton
02d1050130 Revert "Added Ports information to the expanded view"
This reverts commit b2165b6a29.
2017-05-15 06:53:15 +02:00
Kenan Rhoton
ccb44c964c Moved port Info to be fetched from the standard inspect call to avoid superfluous calls. Also moved the information to the info section instead of a whole new section in the expanded view 2017-05-15 06:52:38 +02:00
Kenan Rhoton
b2165b6a29 Added Ports information to the expanded view 2017-05-11 07:38:12 +02:00
bradley
d81d10ec27 Merge pull request #69 from vielmetti/patch-1
WORK IN PROGRESS Add arm64 build
2017-04-10 10:15:51 +08:00
Edward Vielmetti
9529c04680 Update Makefile
Foolish typo; several lines changed, to make future additions more obvious.
2017-04-07 17:43:18 -04:00
Edward Vielmetti
6a89c9af38 WORK IN PROGRESS Add arm64 build
This change would close #68 and would add an arm64 (ARMv8) build.
2017-04-07 02:10:37 -04:00
Bradley Cicenas
06a29fc912 fix circleci build
circleci apparently does not allow containers/volumes to be removed
2017-04-06 20:58:54 +08:00
Bradley Cicenas
2cba7253fc use create container in makefile 2017-04-03 20:51:05 +08:00
Bradley Cicenas
47d60fe51b gofmt main package 2017-03-28 13:57:30 +10:00
bradley
28f16c9a17 Merge pull request #62 from yashpatel5400/yash
Added page up/down features
2017-03-27 10:09:44 +10:00
yashpatel5400
6560768e08 Added page up/down features 2017-03-23 22:31:08 -04:00
Bradley Cicenas
084c0c4ec8 add global Shutdown() method for exit cleanup 2017-03-23 16:48:25 +10:00
87 changed files with 4591 additions and 1052 deletions

19
.circleci/config.yml Normal file
View 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

2
.gitignore vendored
View File

@@ -1,2 +1,4 @@
ctop ctop
.idea .idea
/vendor/
*.log

View File

@@ -1,3 +1,17 @@
FROM quay.io/vektorcloud/go:1.15
RUN apk add --no-cache make
WORKDIR /app
COPY go.mod .
RUN go mod download
COPY . .
RUN make build && \
mkdir -p /go/bin && \
mv -v ctop /go/bin/
FROM scratch FROM scratch
COPY ./ctop /ctop ENV TERM=linux
COPY --from=0 /go/bin/ctop /ctop
ENTRYPOINT ["/ctop"] ENTRYPOINT ["/ctop"]

View File

@@ -1,12 +0,0 @@
FROM quay.io/vektorcloud/go:1.8
RUN apk add --no-cache make
COPY glide.* /go/src/github.com/bcicen/ctop/
WORKDIR /go/src/github.com/bcicen/ctop/
RUN glide install
COPY . /go/src/github.com/bcicen/ctop
RUN make build && \
mkdir -p /go/bin && \
mv -v ctop /go/bin/

View File

@@ -4,31 +4,33 @@ BUILD=$(shell git rev-parse --short HEAD)
LD_FLAGS="-w -X main.version=$(VERSION) -X main.build=$(BUILD)" LD_FLAGS="-w -X main.version=$(VERSION) -X main.build=$(BUILD)"
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)"
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 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_build -f Dockerfile_build .
docker run -ti --rm -v $(shell pwd):/target ctop_build cp -v /go/bin/ctop /target/
docker build -t ctop -f Dockerfile . docker build -t ctop -f Dockerfile .
release: release:
mkdir release mkdir release
go get github.com/progrium/gh-release/... cp _build/* release
cp build/* release cd release; sha256sum --quiet --check sha256sums.txt && \
gh-release create bcicen/$(NAME) $(VERSION) \ gh release create $(VERSION) -d -t v$(VERSION) *
$(shell git rev-parse --abbrev-ref HEAD) $(VERSION)
.PHONY: build .PHONY: build

102
README.md
View File

@@ -2,23 +2,39 @@
# #
![release][release] ![homebrew][homebrew] ![macports][macports]
Top-like interface for container metrics 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` currently comes with built-in support for Docker; connectors for other container and cluster systems are planned for future releases. `ctop` comes with built-in support for Docker and runC; connectors for other container and cluster systems are planned for future releases.
## Install ## Install
Fetch the [latest release](https://github.com/bcicen/ctop/releases) for your platform: Fetch the [latest release](https://github.com/bcicen/ctop/releases) for your platform:
#### Linux #### Debian/Ubuntu
Maintained by a [third party](https://packages.azlux.fr/)
```bash
echo "deb http://packages.azlux.fr/debian/ buster main" | sudo tee /etc/apt/sources.list.d/azlux.list
wget -qO - https://azlux.fr/repo.gpg.key | sudo apt-key add -
sudo apt update
sudo apt install docker-ctop
```
#### Arch
`ctop` is available for Arch in the [AUR](https://aur.archlinux.org/packages/ctop-bin/)
#### Linux (Generic)
```bash ```bash
sudo wget https://github.com/bcicen/ctop/releases/download/v0.5/ctop-0.5-linux-amd64 -O /usr/local/bin/ctop sudo wget https://github.com/bcicen/ctop/releases/download/v0.7.6/ctop-0.7.6-linux-amd64 -O /usr/local/bin/ctop
sudo chmod +x /usr/local/bin/ctop sudo chmod +x /usr/local/bin/ctop
``` ```
@@ -29,16 +45,22 @@ brew install ctop
``` ```
or or
```bash ```bash
sudo curl -Lo /usr/local/bin/ctop https://github.com/bcicen/ctop/releases/download/v0.5/ctop-0.5-darwin-amd64 sudo port install ctop
```
or
```bash
sudo curl -Lo /usr/local/bin/ctop https://github.com/bcicen/ctop/releases/download/v0.7.6/ctop-0.7.6-darwin-amd64
sudo chmod +x /usr/local/bin/ctop sudo chmod +x /usr/local/bin/ctop
``` ```
or run via Docker: #### Docker
```bash
docker run -ti --name ctop --rm -v /var/run/docker.sock:/var/run/docker.sock quay.io/vektorlab/ctop:latest
```
`ctop` is also available for Arch in the [AUR](https://aur.archlinux.org/packages/ctop-bin/) ```bash
docker run --rm -ti \
--name=ctop \
--volume /var/run/docker.sock:/var/run/docker.sock:ro \
quay.io/vektorlab/ctop:latest
```
## Building ## Building
@@ -46,35 +68,51 @@ Build steps can be found [here][build].
## Usage ## Usage
`ctop` requires no arguments and will configure itself using the `DOCKER_HOST` environment variable `ctop` requires no arguments and uses Docker host variables by default. See [connectors][connectors] for further configuration options.
```bash
export DOCKER_HOST=tcp://127.0.0.1:4243 ### Config file
ctop
``` 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 `-v` | output version information and exit
### Keybindings ### Keybindings
Key | Action | Key | Action |
--- | --- | :----------------------: | ---------------------------------------------------------- |
a | Toggle display of all (running and non-running) containers | <kbd>&lt;ENTER&gt;</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
[expanded_view]: _docs/expanded.md [connectors]: _docs/connectors.md
[single_view]: _docs/single.md
[release]: https://img.shields.io/github/release/bcicen/ctop.svg "ctop"
[homebrew]: https://img.shields.io/homebrew/v/ctop.svg "ctop"
[macports]: https://repology.org/badge/version-for-repo/macports/ctop.svg?header=macports "ctop"
## Alternatives
See [Awesome Docker list](https://github.com/veggiemonk/awesome-docker/blob/master/README.md#terminal) for similar tools to work with Docker.

View File

@@ -1 +1 @@
0.5.1 0.7.6

View File

@@ -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
``` ```

26
_docs/connectors.md Normal file
View File

@@ -0,0 +1,26 @@
# Connectors
`ctop` comes with the below native connectors, enabled via the `--connector` option.
Default connector behavior can be changed by setting the relevant environment variables.
## Docker
Default connector, configurable via standard [Docker commandline varaibles](https://docs.docker.com/engine/reference/commandline/cli/#environment-variables)
#### Options
Var | Description
--- | ---
DOCKER_HOST | Daemon socket to connect to (default: `unix://var/run/docker.sock`)
## RunC
Using this connector requires full privileges to the local runC root dir of container state (default: `/run/runc`)
#### Options
Var | Description
--- | ---
RUNC_ROOT | path to runc root for container state (default: `/run/runc`)
RUNC_SYSTEMD_CGROUP | if set, enable systemd cgroups

View File

@@ -1,14 +1,23 @@
# Debug Mode # Debug Mode
`ctop` comes with a built-in logging facility and local socket server to simplify debugging at run time. Debug mode can be enabled via the `CTOP_DEBUG` environment variable: `ctop` comes with a built-in logging facility and local socket server to simplify debugging at run time.
## Quick Start
If running `ctop` via Docker, debug logging can be most easily enabled as below:
```bash ```bash
CTOP_DEBUG=1 ./ctop docker run -ti --rm \
--name=ctop \
-e CTOP_DEBUG=1 \
-e CTOP_DEBUG_TCP=1 \
-p 9000:9000 \
-v /var/run/docker.sock:/var/run/docker.sock \
quay.io/vektorlab/ctop:latest
``` ```
While `ctop` is running, you can connect to the logging socket via socat or similar tools: Log messages can be followed by connecting to the default listen address:
```bash ```bash
socat unix-connect:/tmp/ctop.sock stdio curl -s localhost:9000
``` ```
example output: example output:
@@ -22,3 +31,41 @@ example output:
15:06:43.883 ▶ INFO 008 collector started for container: 7120f83ca... 15:06:43.883 ▶ INFO 008 collector started for container: 7120f83ca...
... ...
``` ```
## Unix Socket
Debug mode is enabled via the `CTOP_DEBUG` environment variable:
```bash
CTOP_DEBUG=1 ./ctop
```
While `ctop` is running, you can connect to the logging socket via socat or similar tools:
```bash
socat unix-connect:./ctop.sock stdio
```
## TCP Logging Socket
In lieu of using a local unix socket, TCP logging can be enabled via the `CTOP_DEBUG_TCP` environment variable:
```bash
CTOP_DEBUG=1 CTOP_DEBUG_TCP=1 ./ctop
```
A TCP listener for streaming log messages will be started on the default listen address(`0.0.0.0:9000`)
## Log to file
You can also log to a file by specifying `CTOP_DEBUG_FILE=/path/to/ctop.log` environment variable:
```sh
CTOP_DEBUG=1 CTOP_DEBUG_FILE=ctop.log ./ctop
```
This is useful for GoLand to see logs right in debug panel:
* Edit Run configuration
* Go to Logs tab
* Specify this log file in "Log file to be shown in console".
Then during debugging you'll see the log tab in debug panel:
![Debug in GoLand](img/goland_debug.png)

View File

@@ -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>

BIN
_docs/img/goland_debug.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

View File

Before

Width:  |  Height:  |  Size: 549 KiB

After

Width:  |  Height:  |  Size: 549 KiB

BIN
_docs/img/status.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

4
_docs/single.md Normal file
View 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
View 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>

View File

@@ -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}

View File

@@ -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
} }

170
config/columns.go Normal file
View File

@@ -0,0 +1,170 @@
package config
import (
"strings"
)
// defaults
var defaultColumns = []Column{
{
Name: "status",
Label: "Status Indicator",
Enabled: true,
},
{
Name: "name",
Label: "Container Name",
Enabled: true,
},
{
Name: "id",
Label: "Container ID",
Enabled: true,
},
{
Name: "image",
Label: "Image name",
Enabled: false,
},
{
Name: "ports",
Label: "Exposed ports",
Enabled: false,
},
{
Name: "IPs",
Label: "Exposed IPs",
Enabled: false,
},
{
Name: "created",
Label: "Date created",
Enabled: false,
},
{
Name: "cpu",
Label: "CPU Usage",
Enabled: true,
},
{
Name: "cpus",
Label: "CPU Usage (% of system total)",
Enabled: false,
},
{
Name: "mem",
Label: "Memory Usage",
Enabled: true,
},
{
Name: "net",
Label: "Network RX/TX",
Enabled: true,
},
{
Name: "io",
Label: "Disk IO Read/Write",
Enabled: true,
},
{
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
}

144
config/file.go Normal file
View File

@@ -0,0 +1,144 @@
package config
import (
"fmt"
"os"
"path/filepath"
"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, s)
}
}
SetColumns(colNames)
}
return nil
}
func Write() (path string, err error) {
path, err = getConfigPath()
if err != nil {
return path, err
}
cfgdir := filepath.Dir(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
}

View File

@@ -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)
} }
} }

View File

@@ -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,11 @@ var params = []*Param{
Val: "state", Val: "state",
Label: "Container Sort Field", Label: "Container Sort Field",
}, },
&Param{
Key: "columns",
Val: "status,name,id,cpu,mem,net,io,pids",
Label: "Enabled Columns",
},
} }
type Param struct { type Param struct {
@@ -22,6 +27,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 +38,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 +46,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)
} }

View File

@@ -1,21 +1,26 @@
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",
}, },
} }
@@ -25,8 +30,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 +43,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)
} }

View File

@@ -1,16 +1,17 @@
package metrics package collector
import ( import (
"github.com/bcicen/ctop/models"
api "github.com/fsouza/go-dockerclient" api "github.com/fsouza/go-dockerclient"
) )
// Docker collector // Docker collector
type Docker struct { type Docker struct {
Metrics models.Metrics
id string id string
client *api.Client client *api.Client
running bool running bool
stream chan Metrics stream chan models.Metrics
done chan bool done chan bool
lastCpu float64 lastCpu float64
lastSysCpu float64 lastSysCpu float64
@@ -18,7 +19,7 @@ type Docker struct {
func NewDocker(client *api.Client, id string) *Docker { func NewDocker(client *api.Client, id string) *Docker {
return &Docker{ return &Docker{
Metrics: Metrics{}, Metrics: models.Metrics{},
id: id, id: id,
client: client, client: client,
} }
@@ -26,7 +27,7 @@ func NewDocker(client *api.Client, id string) *Docker {
func (c *Docker) Start() { func (c *Docker) Start() {
c.done = make(chan bool) c.done = make(chan bool)
c.stream = make(chan Metrics) c.stream = make(chan models.Metrics)
stats := make(chan *api.Stats) stats := make(chan *api.Stats)
go func() { go func() {
@@ -60,33 +61,39 @@ func (c *Docker) Running() bool {
return c.running return c.running
} }
func (c *Docker) Stream() chan Metrics { func (c *Docker) Stream() chan models.Metrics {
return c.stream return c.stream
} }
func (c *Docker) Logs() LogCollector {
return NewDockerLogs(c.id, c.client)
}
// Stop collector // Stop collector
func (c *Docker) Stop() { func (c *Docker) Stop() {
c.running = false
c.done <- true c.done <- true
} }
func (c *Docker) ReadCPU(stats *api.Stats) { func (c *Docker) ReadCPU(stats *api.Stats) {
ncpus := float64(len(stats.CPUStats.CPUUsage.PercpuUsage)) ncpus := uint8(len(stats.CPUStats.CPUUsage.PercpuUsage))
total := float64(stats.CPUStats.CPUUsage.TotalUsage) total := float64(stats.CPUStats.CPUUsage.TotalUsage)
system := float64(stats.CPUStats.SystemCPUUsage) system := float64(stats.CPUStats.SystemCPUUsage)
cpudiff := total - c.lastCpu cpudiff := total - c.lastCpu
syscpudiff := system - c.lastSysCpu syscpudiff := system - c.lastSysCpu
c.CPUUtil = round((cpudiff / syscpudiff * 100) * ncpus) c.NCpus = ncpus
c.CPUUtil = percent(cpudiff, syscpudiff)
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 = round((float64(c.MemUsage) / float64(c.MemLimit)) * 100) c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
} }
func (c *Docker) ReadNet(stats *api.Stats) { func (c *Docker) ReadNet(stats *api.Stats) {
@@ -102,10 +109,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

View File

@@ -0,0 +1,99 @@
package collector
import (
"bufio"
"context"
"io"
"strings"
"time"
"github.com/bcicen/ctop/models"
api "github.com/fsouza/go-dockerclient"
)
type DockerLogs struct {
id string
client *api.Client
done chan bool
}
func NewDockerLogs(id string, client *api.Client) *DockerLogs {
return &DockerLogs{
id: id,
client: client,
done: make(chan bool),
}
}
func (l *DockerLogs) Stream() chan models.Log {
r, w := io.Pipe()
logCh := make(chan models.Log)
ctx, cancel := context.WithCancel(context.Background())
opts := api.LogsOptions{
Context: ctx,
Container: l.id,
OutputStream: w,
//ErrorStream: w,
Stdout: true,
Stderr: true,
Tail: "20",
Follow: true,
Timestamps: true,
RawTerminal: true,
}
// read io pipe into channel
go func() {
scanner := bufio.NewScanner(r)
for scanner.Scan() {
parts := strings.SplitN(scanner.Text(), " ", 2)
ts := l.parseTime(parts[0])
logCh <- models.Log{Timestamp: ts, Message: parts[1]}
}
}()
// connect to container log stream
go func() {
err := l.client.Logs(opts)
if err != nil {
log.Errorf("error reading container logs: %s", err)
}
log.Infof("log reader stopped for container: %s", l.id)
}()
go func() {
<-l.done
cancel()
}()
log.Infof("log reader started for container: %s", l.id)
return logCh
}
func (l *DockerLogs) Stop() { l.done <- true }
func (l *DockerLogs) parseTime(s string) time.Time {
ts, err := time.Parse(time.RFC3339Nano, s)
if err == nil {
return ts
}
ts, err2 := time.Parse(time.RFC3339Nano, 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
}

View File

@@ -0,0 +1,35 @@
package collector
import (
"math"
"github.com/bcicen/ctop/logging"
"github.com/bcicen/ctop/models"
)
var log = logging.Init()
type LogCollector interface {
Stream() chan models.Log
Stop()
}
type Collector interface {
Stream() chan models.Metrics
Logs() LogCollector
Running() bool
Start()
Stop()
}
func round(num float64) int {
return int(num + math.Copysign(0.5, num))
}
// return rounded percentage
func percent(val float64, total float64) int {
if total <= 0 {
return 0
}
return round((val / total) * 100)
}

View File

@@ -1,16 +1,18 @@
// +build !release // +build !release
package metrics package collector
import ( import (
"math/rand" "math/rand"
"time" "time"
"github.com/bcicen/ctop/models"
) )
// Mock collector // Mock collector
type Mock struct { type Mock struct {
Metrics models.Metrics
stream chan Metrics stream chan models.Metrics
done bool done bool
running bool running bool
aggression int64 aggression int64
@@ -18,7 +20,7 @@ type Mock struct {
func NewMock(a int64) *Mock { func NewMock(a int64) *Mock {
c := &Mock{ c := &Mock{
Metrics: Metrics{}, Metrics: models.Metrics{},
aggression: a, aggression: a,
} }
c.MemLimit = 2147483648 c.MemLimit = 2147483648
@@ -31,23 +33,33 @@ func (c *Mock) Running() bool {
func (c *Mock) Start() { func (c *Mock) Start() {
c.done = false c.done = false
c.stream = make(chan Metrics) c.stream = make(chan models.Metrics)
go c.run() go c.run()
} }
func (c *Mock) Stop() { func (c *Mock) Stop() {
c.running = false
c.done = true c.done = true
} }
func (c *Mock) Stream() chan Metrics { func (c *Mock) Stream() chan models.Metrics {
return c.stream return c.stream
} }
func (c *Mock) Logs() LogCollector {
return &MockLogs{make(chan bool)}
}
func (c *Mock) run() { func (c *Mock) run() {
c.running = true c.running = true
rand.Seed(int64(time.Now().Nanosecond())) rand.Seed(int64(time.Now().Nanosecond()))
defer close(c.stream) defer close(c.stream)
// set to random static value, once
c.Pids = rand.Intn(12)
c.IOBytesRead = rand.Int63n(8098) * c.aggression
c.IOBytesWrite = rand.Int63n(8098) * c.aggression
for { for {
c.CPUUtil += rand.Intn(2) * int(c.aggression) c.CPUUtil += rand.Intn(2) * int(c.aggression)
if c.CPUUtil >= 100 { if c.CPUUtil >= 100 {
@@ -60,7 +72,7 @@ func (c *Mock) run() {
if c.MemUsage > c.MemLimit { if c.MemUsage > c.MemLimit {
c.MemUsage = 0 c.MemUsage = 0
} }
c.MemPercent = round((float64(c.MemUsage) / float64(c.MemLimit)) * 100) c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
c.stream <- c.Metrics c.stream <- c.Metrics
if c.done { if c.done {
break break

View File

@@ -0,0 +1,31 @@
package collector
import (
"time"
"github.com/bcicen/ctop/models"
)
const mockLog = "Cura ob pro qui tibi inveni dum qua fit donec amare illic mea, regem falli contexo pro peregrinorum heremo absconditi araneae meminerim deliciosas actionibus facere modico dura sonuerunt psalmi contra rerum, tempus mala anima volebant dura quae o modis."
type MockLogs struct {
done chan bool
}
func (l *MockLogs) Stream() chan models.Log {
logCh := make(chan models.Log)
go func() {
for {
select {
case <-l.done:
break
default:
logCh <- models.Log{Timestamp: time.Now(), Message: mockLog}
time.Sleep(250 * time.Millisecond)
}
}
}()
return logCh
}
func (l *MockLogs) Stop() { l.done <- true }

View File

@@ -0,0 +1,45 @@
// +build linux
package collector
import (
linuxproc "github.com/c9s/goprocinfo/linux"
)
var sysMemTotal = getSysMemTotal()
const (
clockTicksPerSecond uint64 = 100
nanoSecondsPerSecond = 1e9
)
func getSysMemTotal() int64 {
stat, err := linuxproc.ReadMemInfo("/proc/meminfo")
if err != nil {
log.Errorf("error reading system stats: %s", err)
return 0
}
return int64(stat.MemTotal * 1024)
}
// return cumulative system cpu usage in nanoseconds
func getSysCPUUsage() uint64 {
stat, err := linuxproc.ReadStat("/proc/stat")
if err != nil {
log.Errorf("error reading system stats: %s", err)
return 0
}
sum := stat.CPUStatAll.User +
stat.CPUStatAll.Nice +
stat.CPUStatAll.System +
stat.CPUStatAll.Idle +
stat.CPUStatAll.IOWait +
stat.CPUStatAll.IRQ +
stat.CPUStatAll.SoftIRQ +
stat.CPUStatAll.Steal +
stat.CPUStatAll.Guest +
stat.CPUStatAll.GuestNice
return (sum * nanoSecondsPerSecond) / clockTicksPerSecond
}

132
connector/collector/runc.go Normal file
View File

@@ -0,0 +1,132 @@
// +build linux
package collector
import (
"time"
"github.com/opencontainers/runc/libcontainer"
"github.com/opencontainers/runc/libcontainer/cgroups"
"github.com/opencontainers/runc/types"
"github.com/bcicen/ctop/models"
)
// Runc collector
type Runc struct {
models.Metrics
id string
libc libcontainer.Container
stream chan models.Metrics
done bool
running bool
interval int // collection interval, in seconds
lastCpu float64
lastSysCpu float64
}
func NewRunc(libc libcontainer.Container) *Runc {
c := &Runc{
Metrics: models.Metrics{},
id: libc.ID(),
libc: libc,
interval: 1,
}
return c
}
func (c *Runc) Running() bool {
return c.running
}
func (c *Runc) Start() {
c.done = false
c.stream = make(chan models.Metrics)
go c.run()
}
func (c *Runc) Stop() {
c.running = false
c.done = true
}
func (c *Runc) Stream() chan models.Metrics {
return c.stream
}
func (c *Runc) Logs() LogCollector {
return nil
}
func (c *Runc) run() {
c.running = true
defer close(c.stream)
log.Debugf("collector started for container: %s", c.id)
for {
stats, err := c.libc.Stats()
if err != nil {
log.Errorf("failed to collect stats for container %s:\n%s", c.id, err)
break
}
c.ReadCPU(stats.CgroupStats)
c.ReadMem(stats.CgroupStats)
c.ReadNet(stats.Interfaces)
c.stream <- c.Metrics
if c.done {
break
}
time.Sleep(1 * time.Second)
}
c.running = false
}
func (c *Runc) ReadCPU(stats *cgroups.Stats) {
u := stats.CpuStats.CpuUsage
ncpus := uint8(len(u.PercpuUsage))
total := float64(u.TotalUsage)
system := float64(getSysCPUUsage())
cpudiff := total - c.lastCpu
syscpudiff := system - c.lastSysCpu
c.NCpus = ncpus
c.CPUUtil = percent(cpudiff, syscpudiff)
c.lastCpu = total
c.lastSysCpu = system
c.Pids = int(stats.PidsStats.Current)
}
func (c *Runc) ReadMem(stats *cgroups.Stats) {
c.MemUsage = int64(stats.MemoryStats.Usage.Usage)
c.MemLimit = int64(stats.MemoryStats.Usage.Limit)
if c.MemLimit > sysMemTotal && sysMemTotal > 0 {
c.MemLimit = sysMemTotal
}
c.MemPercent = percent(float64(c.MemUsage), float64(c.MemLimit))
}
func (c *Runc) ReadNet(interfaces []*types.NetworkInterface) {
var rx, tx int64
for _, network := range interfaces {
rx += int64(network.RxBytes)
tx += int64(network.TxBytes)
}
c.NetRx, c.NetTx = rx, tx
}
func (c *Runc) ReadIO(stats *cgroups.Stats) {
var read, write int64
for _, blk := range stats.BlkioStats.IoServiceBytesRecursive {
if blk.Op == "Read" {
read = int64(blk.Value)
}
if blk.Op == "Write" {
write = int64(blk.Value)
}
}
c.IOBytesRead, c.IOBytesWrite = read, write
}

292
connector/docker.go Normal file
View File

@@ -0,0 +1,292 @@
package connector
import (
"fmt"
"github.com/op/go-logging"
"strings"
"sync"
"github.com/bcicen/ctop/connector/collector"
"github.com/bcicen/ctop/connector/manager"
"github.com/bcicen/ctop/container"
api "github.com/fsouza/go-dockerclient"
)
func init() { enabled["docker"] = NewDocker }
var actionToStatus = map[string]string{
"start": "running",
"die": "exited",
"stop": "exited",
"pause": "paused",
"unpause": "running",
}
type StatusUpdate struct {
Cid string
Field string // "status" or "health"
Status string
}
type Docker struct {
client *api.Client
containers map[string]*container.Container
needsRefresh chan string // container IDs requiring refresh
statuses chan StatusUpdate
closed chan struct{}
lock sync.RWMutex
}
func NewDocker() (Connector, error) {
// init docker client
client, err := api.NewClientFromEnv()
if err != nil {
return nil, err
}
cm := &Docker{
client: client,
containers: make(map[string]*container.Container),
needsRefresh: make(chan string, 60),
statuses: make(chan StatusUpdate, 60),
closed: make(chan struct{}),
lock: sync.RWMutex{},
}
// query info as pre-flight healthcheck
info, err := client.Info()
if err != nil {
return nil, err
}
log.Debugf("docker-connector ID: %s", info.ID)
log.Debugf("docker-connector Driver: %s", info.Driver)
log.Debugf("docker-connector Images: %d", info.Images)
log.Debugf("docker-connector Name: %s", info.Name)
log.Debugf("docker-connector ServerVersion: %s", info.ServerVersion)
go cm.Loop()
go cm.LoopStatuses()
cm.refreshAll()
go cm.watchEvents()
return cm, nil
}
// Docker implements Connector
func (cm *Docker) Wait() struct{} { return <-cm.closed }
// Docker events watcher
func (cm *Docker) watchEvents() {
log.Info("docker event listener starting")
events := make(chan *api.APIEvents)
cm.client.AddEventListener(events)
for e := range events {
if e.Type != "container" {
continue
}
actionName := e.Action
// fast skip all exec_* events: exec_create, exec_start, exec_die
if strings.HasPrefix(actionName, "exec_") {
continue
}
// Action may have additional param i.e. "health_status: healthy"
// We need to strip to have only action name
sepIdx := strings.Index(actionName, ": ")
if sepIdx != -1 {
actionName = actionName[:sepIdx]
}
switch actionName {
// most frequent event is a health checks
case "health_status":
healthStatus := e.Action[sepIdx+2:]
if log.IsEnabledFor(logging.DEBUG) {
log.Debugf("handling docker event: action=health_status id=%s %s", e.ID, healthStatus)
}
cm.statuses <- StatusUpdate{e.ID, "health", healthStatus}
case "create":
if log.IsEnabledFor(logging.DEBUG) {
log.Debugf("handling docker event: action=create id=%s", e.ID)
}
cm.needsRefresh <- e.ID
case "destroy":
if log.IsEnabledFor(logging.DEBUG) {
log.Debugf("handling docker event: action=destroy id=%s", e.ID)
}
cm.delByID(e.ID)
default:
// check if this action changes status e.g. start -> running
status := actionToStatus[actionName]
if status != "" {
if log.IsEnabledFor(logging.DEBUG) {
log.Debugf("handling docker event: action=%s id=%s %s", actionName, e.ID, status)
}
cm.statuses <- StatusUpdate{e.ID, "status", status}
}
}
}
log.Info("docker event listener exited")
close(cm.closed)
}
func portsFormat(ports map[api.Port][]api.PortBinding) string {
var exposed []string
var published []string
for k, v := range ports {
if len(v) == 0 {
exposed = append(exposed, string(k))
continue
}
for _, binding := range v {
s := fmt.Sprintf("%s:%s -> %s", binding.HostIP, binding.HostPort, k)
published = append(published, s)
}
}
return strings.Join(append(exposed, published...), "\n")
}
func ipsFormat(networks map[string]api.ContainerNetwork) string {
var ips []string
for k, v := range networks {
s := fmt.Sprintf("%s:%s", k, v.IPAddress)
ips = append(ips, s)
}
return strings.Join(ips, "\n")
}
func (cm *Docker) refresh(c *container.Container) {
insp, found, failed := cm.inspect(c.Id)
if failed {
return
}
// remove container if no longer exists
if !found {
cm.delByID(c.Id)
return
}
c.SetMeta("name", shortName(insp.Name))
c.SetMeta("image", insp.Config.Image)
c.SetMeta("IPs", ipsFormat(insp.NetworkSettings.Networks))
c.SetMeta("ports", portsFormat(insp.NetworkSettings.Ports))
c.SetMeta("created", insp.Created.Format("Mon Jan 2 15:04:05 2006"))
c.SetMeta("health", insp.State.Health.Status)
c.SetMeta("[ENV-VAR]", strings.Join(insp.Config.Env, ";"))
c.SetState(insp.State.Status)
}
func (cm *Docker) inspect(id string) (insp *api.Container, found bool, failed bool) {
c, err := cm.client.InspectContainer(id)
if err != nil {
if _, notFound := err.(*api.NoSuchContainer); notFound {
return c, false, false
}
// other error e.g. connection failed
log.Errorf("%s (%T)", err.Error(), err)
return c, false, true
}
return c, true, false
}
// Mark all container IDs for refresh
func (cm *Docker) refreshAll() {
opts := api.ListContainersOptions{All: true}
allContainers, err := cm.client.ListContainers(opts)
if err != nil {
log.Errorf("%s (%T)", err.Error(), err)
return
}
for _, i := range allContainers {
c := cm.MustGet(i.ID)
c.SetMeta("name", shortName(i.Names[0]))
c.SetState(i.State)
cm.needsRefresh <- c.Id
}
}
func (cm *Docker) Loop() {
for {
select {
case id := <-cm.needsRefresh:
c := cm.MustGet(id)
cm.refresh(c)
case <-cm.closed:
return
}
}
}
func (cm *Docker) LoopStatuses() {
for {
select {
case statusUpdate := <-cm.statuses:
c, _ := cm.Get(statusUpdate.Cid)
if c != nil {
if statusUpdate.Field == "health" {
c.SetMeta("health", statusUpdate.Status)
} else {
c.SetState(statusUpdate.Status)
}
}
case <-cm.closed:
return
}
}
}
// MustGet gets a single container, creating one anew if not existing
func (cm *Docker) MustGet(id string) *container.Container {
c, ok := cm.Get(id)
// append container struct for new containers
if !ok {
// create collector
collector := collector.NewDocker(cm.client, id)
// create manager
manager := manager.NewDocker(cm.client, id)
// create container
c = container.New(id, collector, manager)
cm.lock.Lock()
cm.containers[id] = c
cm.lock.Unlock()
}
return c
}
// Docker implements Connector
func (cm *Docker) Get(id string) (*container.Container, bool) {
cm.lock.Lock()
c, ok := cm.containers[id]
cm.lock.Unlock()
return c, ok
}
// Remove containers by ID
func (cm *Docker) delByID(id string) {
cm.lock.Lock()
delete(cm.containers, id)
cm.lock.Unlock()
log.Infof("removed dead container: %s", id)
}
// Docker implements Connector
func (cm *Docker) All() (containers container.Containers) {
cm.lock.Lock()
for _, c := range cm.containers {
containers = append(containers, c)
}
containers.Sort()
containers.Filter()
cm.lock.Unlock()
return containers
}
// use primary container name
func shortName(name string) string {
return strings.TrimPrefix(name, "/")
}

104
connector/main.go Normal file
View File

@@ -0,0 +1,104 @@
package connector
import (
"fmt"
"sort"
"sync"
"time"
"github.com/bcicen/ctop/container"
"github.com/bcicen/ctop/logging"
)
var (
log = logging.Init()
enabled = make(map[string]ConnectorFn)
)
type ConnectorFn func() (Connector, error)
type Connector interface {
// All returns a pre-sorted container.Containers of all discovered containers
All() container.Containers
// Get returns a single container.Container by ID
Get(string) (*container.Container, bool)
// Wait blocks until the underlying connection is lost
Wait() struct{}
}
// ConnectorSuper provides initial connection and retry on failure for
// an undlerying Connector type
type ConnectorSuper struct {
conn Connector
connFn ConnectorFn
err error
lock sync.RWMutex
}
func NewConnectorSuper(connFn ConnectorFn) *ConnectorSuper {
cs := &ConnectorSuper{
connFn: connFn,
err: fmt.Errorf("connecting..."),
}
go cs.loop()
return cs
}
// Get returns the underlying Connector, or nil and an error
// if the Connector is not yet initialized or is disconnected.
func (cs *ConnectorSuper) Get() (Connector, error) {
cs.lock.RLock()
defer cs.lock.RUnlock()
if cs.err != nil {
return nil, cs.err
}
return cs.conn, nil
}
func (cs *ConnectorSuper) setError(err error) {
cs.lock.Lock()
defer cs.lock.Unlock()
cs.err = err
}
func (cs *ConnectorSuper) loop() {
const interval = 3
for {
log.Infof("initializing connector")
conn, err := cs.connFn()
if err != nil {
cs.setError(err)
log.Errorf("failed to initialize connector: %s (%T)", err, err)
log.Errorf("retrying in %ds", interval)
time.Sleep(interval * time.Second)
} else {
cs.conn = conn
cs.setError(nil)
log.Infof("successfully initialized connector")
// wait until connection closed
cs.conn.Wait()
cs.setError(fmt.Errorf("attempting to reconnect..."))
log.Infof("connector closed")
}
}
}
// Enabled returns names for all enabled connectors on the current platform
func Enabled() (a []string) {
for k, _ := range enabled {
a = append(a, k)
}
sort.Strings(a)
return a
}
// ByName returns a ConnectorSuper for a given name, or error if the connector
// does not exists on the current platform
func ByName(s string) (*ConnectorSuper, error) {
if cfn, ok := enabled[s]; ok {
return NewConnectorSuper(cfn), nil
}
return nil, fmt.Errorf("invalid connector type \"%s\"", s)
}

150
connector/manager/docker.go Normal file
View 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
View 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
View 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
View 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
}

View File

@@ -1,52 +1,81 @@
// +build !release // +build !release
package main package connector
import ( import (
"math/rand" "math/rand"
"sort"
"strings" "strings"
"time" "time"
"github.com/bcicen/ctop/metrics" "github.com/bcicen/ctop/connector/collector"
"github.com/bcicen/ctop/connector/manager"
"github.com/bcicen/ctop/container"
"github.com/jgautheron/codename-generator" "github.com/jgautheron/codename-generator"
"github.com/nu7hatch/gouuid" "github.com/nu7hatch/gouuid"
) )
type MockContainerSource struct { func init() { enabled["mock"] = NewMock }
containers Containers
type Mock struct {
containers container.Containers
} }
func NewMockContainerSource() *MockContainerSource { func NewMock() (Connector, error) {
cs := &MockContainerSource{} 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
func (cs *MockContainerSource) Init() { 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 *MockContainerSource) makeContainer(aggression int64) { func (cs *Mock) Wait() struct{} {
collector := metrics.NewMock(aggression) ch := make(chan struct{})
c := NewContainer(makeID(), collector) go func() {
time.Sleep(30 * time.Second)
close(ch)
}()
return <-ch
}
var healthStates = []string{"starting", "healthy", "unhealthy"}
func (cs *Mock) makeContainer(aggression int64, health bool) {
collector := collector.NewMock(aggression)
manager := manager.NewMock()
c := container.New(makeID(), collector, manager)
c.SetMeta("name", makeName()) c.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)
} }
func (cs *MockContainerSource) Loop() { func (cs *Mock) Loop() {
iter := 0 iter := 0
for { for {
// Change state for random container // Change state for random container
@@ -60,7 +89,7 @@ func (cs *MockContainerSource) Loop() {
} }
// Get a single container, by ID // Get a single container, by ID
func (cs *MockContainerSource) Get(id string) (*Container, bool) { func (cs *Mock) Get(id string) (*container.Container, bool) {
for _, c := range cs.containers { for _, c := range cs.containers {
if c.Id == id { if c.Id == id {
return c, true return c, true
@@ -69,15 +98,15 @@ func (cs *MockContainerSource) Get(id string) (*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 *MockContainerSource) All() Containers { func (cs *Mock) All() container.Containers {
sort.Sort(cs.containers) cs.containers.Sort()
cs.containers.Filter() cs.containers.Filter()
return cs.containers return cs.containers
} }
// Remove containers by ID // Remove containers by ID
func (cs *MockContainerSource) delByID(id string) { func (cs *Mock) delByID(id string) {
for n, c := range cs.containers { for n, c := range cs.containers {
if c.Id == id { if c.Id == id {
cs.del(n) cs.del(n)
@@ -87,7 +116,7 @@ func (cs *MockContainerSource) delByID(id string) {
} }
// Remove one or more containers by index // Remove one or more containers by index
func (cs *MockContainerSource) del(idx ...int) { func (cs *Mock) del(idx ...int) {
for _, i := range idx { for _, i := range idx {
cs.containers = append(cs.containers[:i], cs.containers[i+1:]...) cs.containers = append(cs.containers[:i], cs.containers[i+1:]...)
} }

257
connector/runc.go Normal file
View File

@@ -0,0 +1,257 @@
// +build linux
package connector
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sync"
"time"
"github.com/bcicen/ctop/connector/collector"
"github.com/bcicen/ctop/connector/manager"
"github.com/bcicen/ctop/container"
"github.com/opencontainers/runc/libcontainer"
"github.com/opencontainers/runc/libcontainer/cgroups/systemd"
)
func init() { enabled["runc"] = NewRunc }
type RuncOpts struct {
root string // runc root path
systemdCgroups bool // use systemd cgroups
}
func NewRuncOpts() (RuncOpts, error) {
var opts RuncOpts
// read runc root path
root := os.Getenv("RUNC_ROOT")
if root == "" {
root = "/run/runc"
}
abs, err := filepath.Abs(root)
if err != nil {
return opts, err
}
opts.root = abs
// ensure runc root path is readable
_, err = ioutil.ReadDir(opts.root)
if err != nil {
return opts, err
}
if os.Getenv("RUNC_SYSTEMD_CGROUP") == "1" {
opts.systemdCgroups = true
}
return opts, nil
}
type Runc struct {
opts RuncOpts
factory libcontainer.Factory
containers map[string]*container.Container
libContainers map[string]libcontainer.Container
closed chan struct{}
needsRefresh chan string // container IDs requiring refresh
lock sync.RWMutex
}
func NewRunc() (Connector, error) {
opts, err := NewRuncOpts()
if err != nil {
return nil, err
}
factory, err := getFactory(opts)
if err != nil {
return nil, err
}
cm := &Runc{
opts: opts,
factory: factory,
containers: make(map[string]*container.Container),
libContainers: make(map[string]libcontainer.Container),
closed: make(chan struct{}),
lock: sync.RWMutex{},
}
go func() {
for {
select {
case <-cm.closed:
return
case <-time.After(5 * time.Second):
cm.refreshAll()
}
}
}()
go cm.Loop()
return cm, nil
}
func (cm *Runc) GetLibc(id string) libcontainer.Container {
// return previously loaded container
libc, ok := cm.libContainers[id]
if ok {
return libc
}
// load container
libc, err := cm.factory.Load(id)
if err != nil {
// remove container if no longer exists
if lerr, ok := err.(libcontainer.Error); ok && lerr.Code() == libcontainer.ContainerNotExists {
cm.delByID(id)
} else {
log.Warningf("failed to read container: %s\n", err)
}
return nil
}
return libc
}
// update a ctop container from libcontainer
func (cm *Runc) refresh(id string) {
libc := cm.GetLibc(id)
if libc == nil {
return
}
c := cm.MustGet(id)
// remove container if entered destroyed state on last refresh
// this gives adequate time for the collector to be shut down
if c.GetMeta("state") == "destroyed" {
cm.delByID(id)
return
}
status, err := libc.Status()
if err != nil {
log.Warningf("failed to read status for container: %s\n", err)
} else {
c.SetState(status.String())
}
state, err := libc.State()
if err != nil {
log.Warningf("failed to read state for container: %s\n", err)
} else {
c.SetMeta("created", state.BaseState.Created.Format("Mon Jan 2 15:04:05 2006"))
}
conf := libc.Config()
c.SetMeta("rootfs", conf.Rootfs)
}
// Read runc root, creating any new containers
func (cm *Runc) refreshAll() {
list, err := ioutil.ReadDir(cm.opts.root)
if err != nil {
log.Errorf("%s (%T)", err.Error(), err)
close(cm.closed)
return
}
for _, i := range list {
if i.IsDir() {
name := i.Name()
// attempt to load
libc := cm.GetLibc(name)
if libc == nil {
continue
}
_ = cm.MustGet(i.Name()) // ensure container exists
}
}
// queue all existing containers for refresh
for id, _ := range cm.containers {
cm.needsRefresh <- id
}
log.Debugf("queued %d containers for refresh", len(cm.containers))
}
func (cm *Runc) Loop() {
for id := range cm.needsRefresh {
cm.refresh(id)
}
}
// MustGet gets a single ctop container in the map matching libc container, creating one anew if not existing
func (cm *Runc) MustGet(id string) *container.Container {
c, ok := cm.Get(id)
if !ok {
libc := cm.GetLibc(id)
// create collector
collector := collector.NewRunc(libc)
// create container
manager := manager.NewRunc()
c = container.New(id, collector, manager)
name := libc.ID()
// set initial metadata
if len(name) > 12 {
name = name[0:12]
}
c.SetMeta("name", name)
// add to map
cm.lock.Lock()
cm.containers[id] = c
cm.libContainers[id] = libc
cm.lock.Unlock()
log.Debugf("saw new container: %s", id)
}
return c
}
// Remove containers by ID
func (cm *Runc) delByID(id string) {
cm.lock.Lock()
delete(cm.containers, id)
delete(cm.libContainers, id)
cm.lock.Unlock()
log.Infof("removed dead container: %s", id)
}
// Runc implements Connector
func (cm *Runc) Wait() struct{} { return <-cm.closed }
// Runc implements Connector
func (cm *Runc) Get(id string) (*container.Container, bool) {
cm.lock.Lock()
defer cm.lock.Unlock()
c, ok := cm.containers[id]
return c, ok
}
// Runc implements Connector
func (cm *Runc) All() (containers container.Containers) {
cm.lock.Lock()
for _, c := range cm.containers {
containers = append(containers, c)
}
containers.Sort()
containers.Filter()
cm.lock.Unlock()
return containers
}
func getFactory(opts RuncOpts) (libcontainer.Factory, error) {
cgroupManager := libcontainer.Cgroupfs
if opts.systemdCgroups {
if systemd.IsRunningSystemd() {
cgroupManager = libcontainer.SystemdCgroups
} else {
return nil, fmt.Errorf("systemd cgroup enabled, but systemd support for managing cgroups is not available")
}
}
return libcontainer.New(opts.root, cgroupManager)
}

View File

@@ -1,76 +0,0 @@
package main
import (
"github.com/bcicen/ctop/cwidgets"
"github.com/bcicen/ctop/cwidgets/compact"
"github.com/bcicen/ctop/metrics"
)
// Metrics and metadata representing a container
type Container struct {
metrics.Metrics
Id string
Meta map[string]string
Widgets *compact.Compact
updater cwidgets.WidgetUpdater
collector metrics.Collector
display bool // display this container in compact view
}
func NewContainer(id string, collector metrics.Collector) *Container {
widgets := compact.NewCompact(id)
return &Container{
Metrics: metrics.NewMetrics(),
Id: id,
Meta: make(map[string]string),
Widgets: widgets,
updater: widgets,
collector: collector,
}
}
func (c *Container) SetUpdater(u cwidgets.WidgetUpdater) {
c.updater = u
for k, v := range c.Meta {
c.updater.SetMeta(k, v)
}
}
func (c *Container) SetMeta(k, v string) {
c.Meta[k] = v
c.updater.SetMeta(k, v)
}
func (c *Container) GetMeta(k string) string {
if v, ok := c.Meta[k]; ok {
return v
}
return ""
}
func (c *Container) SetState(s string) {
c.SetMeta("state", s)
// start collector, if needed
if s == "running" && !c.collector.Running() {
c.collector.Start()
c.Read(c.collector.Stream())
}
// stop collector, if needed
if s != "running" && c.collector.Running() {
c.collector.Stop()
}
}
// Read metric stream, updating widgets
func (c *Container) Read(stream chan metrics.Metrics) {
go func() {
for metrics := range stream {
c.Metrics = metrics
c.updater.SetMetrics(metrics)
}
log.Infof("reader stopped for container: %s", c.Id)
c.Metrics = metrics.NewMetrics()
c.Widgets.Reset()
}()
log.Infof("reader started for container: %s", c.Id)
}

160
container/main.go Normal file
View File

@@ -0,0 +1,160 @@
package container
import (
"github.com/bcicen/ctop/connector/collector"
"github.com/bcicen/ctop/connector/manager"
"github.com/bcicen/ctop/cwidgets"
"github.com/bcicen/ctop/cwidgets/compact"
"github.com/bcicen/ctop/logging"
"github.com/bcicen/ctop/models"
)
var (
log = logging.Init()
)
const (
running = "running"
)
// Metrics and metadata representing a container
type Container struct {
models.Metrics
Id string
Meta models.Meta
Widgets *compact.CompactRow
Display bool // display this container in compact view
updater cwidgets.WidgetUpdater
collector collector.Collector
manager manager.Manager
}
func New(id string, collector collector.Collector, manager manager.Manager) *Container {
widgets := compact.NewCompactRow()
return &Container{
Metrics: models.NewMetrics(),
Id: id,
Meta: models.NewMeta("id", id[:12]),
Widgets: widgets,
updater: widgets,
collector: collector,
manager: manager,
}
}
func (c *Container) RecreateWidgets() {
c.SetUpdater(cwidgets.NullWidgetUpdater{})
c.Widgets = compact.NewCompactRow()
c.SetUpdater(c.Widgets)
}
func (c *Container) SetUpdater(u cwidgets.WidgetUpdater) {
c.updater = u
c.updater.SetMeta(c.Meta)
}
func (c *Container) SetMeta(k, v string) {
c.Meta[k] = v
c.updater.SetMeta(c.Meta)
}
func (c *Container) GetMeta(k string) string {
return c.Meta.Get(k)
}
func (c *Container) SetState(s string) {
c.SetMeta("state", s)
// start collector, if needed
if s == running && !c.collector.Running() {
c.collector.Start()
c.Read(c.collector.Stream())
}
// stop collector, if needed
if s != running && c.collector.Running() {
c.collector.Stop()
}
}
// Logs returns container log collector
func (c *Container) Logs() collector.LogCollector {
return c.collector.Logs()
}
// Read metric stream, updating widgets
func (c *Container) Read(stream chan models.Metrics) {
go func() {
for metrics := range stream {
c.Metrics = metrics
c.updater.SetMetrics(metrics)
}
log.Infof("reader stopped for container: %s", c.Id)
c.Metrics = models.NewMetrics()
c.Widgets.Reset()
}()
log.Infof("reader started for container: %s", c.Id)
}
func (c *Container) Start() {
if c.Meta["state"] != running {
if err := c.manager.Start(); err != nil {
log.Warningf("container %s: %v", c.Id, err)
log.StatusErr(err)
return
}
c.SetState(running)
}
}
func (c *Container) Stop() {
if c.Meta["state"] == running {
if err := c.manager.Stop(); err != nil {
log.Warningf("container %s: %v", c.Id, err)
log.StatusErr(err)
return
}
c.SetState("exited")
}
}
func (c *Container) Remove() {
if err := c.manager.Remove(); err != nil {
log.Warningf("container %s: %v", c.Id, err)
log.StatusErr(err)
}
}
func (c *Container) Pause() {
if c.Meta["state"] == running {
if err := c.manager.Pause(); err != nil {
log.Warningf("container %s: %v", c.Id, err)
log.StatusErr(err)
return
}
c.SetState("paused")
}
}
func (c *Container) Unpause() {
if c.Meta["state"] == "paused" {
if err := c.manager.Unpause(); err != nil {
log.Warningf("container %s: %v", c.Id, err)
log.StatusErr(err)
return
}
c.SetState(running)
}
}
func (c *Container) Restart() {
if c.Meta["state"] == running {
if err := c.manager.Restart(); err != nil {
log.Warningf("container %s: %v", c.Id, err)
log.StatusErr(err)
return
}
}
}
func (c *Container) Exec(cmd []string) error {
return c.manager.Exec(cmd)
}

View File

@@ -1,8 +1,9 @@
package main package container
import ( import (
"fmt" "fmt"
"regexp" "regexp"
"sort"
"github.com/bcicen/ctop/config" "github.com/bcicen/ctop/config"
) )
@@ -89,6 +90,7 @@ func SortFields() (fields []string) {
type Containers []*Container type Containers []*Container
func (a Containers) Sort() { sort.Sort(a) }
func (a Containers) Len() int { return len(a) } func (a Containers) Len() int { return len(a) }
func (a Containers) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a Containers) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a Containers) Less(i, j int) bool { func (a Containers) Less(i, j int) bool {
@@ -104,14 +106,14 @@ func (a Containers) Filter() {
re := regexp.MustCompile(fmt.Sprintf(".*%s", filter)) re := regexp.MustCompile(fmt.Sprintf(".*%s", filter))
for _, c := range a { for _, c := range a {
c.display = true c.Display = true
// Apply name filter // Apply name filter
if re.FindAllString(c.GetMeta("name"), 1) == nil { if re.FindAllString(c.GetMeta("name"), 1) == nil {
c.display = false c.Display = false
} }
// Apply state filter // Apply state filter
if !config.GetSwitchVal("allContainers") && c.GetMeta("state") != "running" { if !config.GetSwitchVal("allContainers") && c.GetMeta("state") != "running" {
c.display = false c.Display = false
} }
} }
} }

129
cursor.go
View File

@@ -1,24 +1,23 @@
package main package main
import ( import (
"math"
"github.com/bcicen/ctop/connector"
"github.com/bcicen/ctop/container"
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
) )
type GridCursor struct { type GridCursor struct {
selectedID string // id of currently selected container selectedID string // id of currently selected container
filtered Containers filtered container.Containers
cSource ContainerSource cSuper *connector.ConnectorSuper
} isScrolling bool // toggled when actively scrolling
func NewGridCursor() *GridCursor {
return &GridCursor{
cSource: NewDockerContainerSource(),
}
} }
func (gc *GridCursor) Len() int { return len(gc.filtered) } func (gc *GridCursor) Len() int { return len(gc.filtered) }
func (gc *GridCursor) Selected() *Container { func (gc *GridCursor) Selected() *container.Container {
idx := gc.Idx() idx := gc.Idx()
if idx < gc.Len() { if idx < gc.Len() {
return gc.filtered[idx] return gc.filtered[idx]
@@ -26,15 +25,21 @@ func (gc *GridCursor) Selected() *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()
gc.filtered = container.Containers{}
// Containers filtered by display bool cSource, err := gc.cSuper.Get()
gc.filtered = Containers{} 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
} }
@@ -42,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 {
@@ -100,6 +104,9 @@ func (gc *GridCursor) ScrollPage() {
} }
func (gc *GridCursor) Up() { func (gc *GridCursor) Up() {
gc.isScrolling = true
defer func() { gc.isScrolling = false }()
idx := gc.Idx() idx := gc.Idx()
if idx <= 0 { // already at top if idx <= 0 { // already at top
return return
@@ -107,15 +114,18 @@ 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)
} }
func (gc *GridCursor) Down() { func (gc *GridCursor) Down() {
gc.isScrolling = true
defer func() { gc.isScrolling = false }()
idx := gc.Idx() idx := gc.Idx()
if idx >= gc.Len()-1 { // already at bottom if idx >= gc.Len()-1 { // already at bottom
return return
@@ -123,10 +133,65 @@ 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)
} }
func (gc *GridCursor) PgUp() {
idx := gc.Idx()
if idx <= 0 { // already at top
return
}
nextidx := int(math.Max(0.0, float64(idx-cGrid.MaxRows())))
if gc.pgCount() > 0 {
cGrid.Offset = int(math.Max(float64(cGrid.Offset-cGrid.MaxRows()),
float64(0)))
}
active := gc.filtered[idx]
next := gc.filtered[nextidx]
active.Widgets.UnHighlight()
gc.selectedID = next.Id
next.Widgets.Highlight()
cGrid.Align()
ui.Render(cGrid)
}
func (gc *GridCursor) PgDown() {
idx := gc.Idx()
if idx >= gc.Len()-1 { // already at bottom
return
}
nextidx := int(math.Min(float64(gc.Len()-1), float64(idx+cGrid.MaxRows())))
if gc.pgCount() > 0 {
cGrid.Offset = int(math.Min(float64(cGrid.Offset+cGrid.MaxRows()),
float64(gc.Len()-cGrid.MaxRows())))
}
active := gc.filtered[idx]
next := gc.filtered[nextidx]
active.Widgets.UnHighlight()
gc.selectedID = next.Id
next.Widgets.Highlight()
cGrid.Align()
ui.Render(cGrid)
}
// number of pages at current row count and term height
func (gc *GridCursor) pgCount() int {
pages := gc.Len() / cGrid.MaxRows()
if gc.Len()%cGrid.MaxRows() > 0 {
pages++
}
return pages
}

View File

@@ -0,0 +1,54 @@
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,
"image": NewImageCol,
"ports": NewPortsCol,
"IPs": NewIpsCol,
"created": NewCreatedCol,
"cpu": NewCPUCol,
"cpus": NewCpuScaledCol,
"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)
}

View File

@@ -1,21 +1,68 @@
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
scaleCpu bool
} }
func NewGaugeCol() *GaugeCol { func NewCPUCol() CompactCol {
g := ui.NewGauge() return &CPUCol{NewGaugeCol("CPU"), false}
}
func NewCpuScaledCol() CompactCol {
return &CPUCol{NewGaugeCol("CPUS"), true}
}
func (w *CPUCol) SetMetrics(m models.Metrics) {
val := m.CPUUtil
w.BarColor = colorScale(val)
if !w.scaleCpu {
val = val * int(m.NCpus)
}
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 +70,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")
} }

View File

@@ -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,32 +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 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
} }
@@ -54,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())
} }
} }

View File

@@ -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)
} }

View File

@@ -1,142 +0,0 @@
package compact
import (
"github.com/bcicen/ctop/logging"
"github.com/bcicen/ctop/metrics"
ui "github.com/gizak/termui"
)
var log = logging.Init()
type Compact struct {
Status *Status
Name *TextCol
Cid *TextCol
Cpu *GaugeCol
Memory *GaugeCol
Net *TextCol
IO *TextCol
Pids *TextCol
X, Y int
Width int
Height int
}
func NewCompact(id string) *Compact {
// truncate container id
if len(id) > 12 {
id = id[:12]
}
row := &Compact{
Status: NewStatus(),
Name: NewTextCol("-"),
Cid: NewTextCol(id),
Cpu: NewGaugeCol(),
Memory: NewGaugeCol(),
Net: NewTextCol("-"),
IO: NewTextCol("-"),
Pids: NewTextCol("-"),
X: 1,
Height: 1,
}
return row
}
//func (row *Compact) ToggleExpand() {
//if row.Height == 1 {
//row.Height = 4
//} else {
//row.Height = 1
//}
//}
func (row *Compact) SetMeta(k, v string) {
switch k {
case "name":
row.Name.Set(v)
case "state":
row.Status.Set(v)
}
}
func (row *Compact) SetMetrics(m metrics.Metrics) {
row.SetCPU(m.CPUUtil)
row.SetNet(m.NetRx, m.NetTx)
row.SetMem(m.MemUsage, m.MemLimit, m.MemPercent)
row.SetIO(m.IOBytesRead, m.IOBytesWrite)
row.SetPids(m.Pids)
}
// Set gauges, counters to default unread values
func (row *Compact) Reset() {
row.Cpu.Reset()
row.Memory.Reset()
row.Net.Reset()
row.IO.Reset()
row.Pids.Reset()
}
func (row *Compact) GetHeight() int {
return row.Height
}
func (row *Compact) SetX(x int) {
row.X = x
}
func (row *Compact) SetY(y int) {
if y == row.Y {
return
}
for _, col := range row.all() {
col.SetY(y)
}
row.Y = y
}
func (row *Compact) SetWidth(width int) {
if width == row.Width {
return
}
x := row.X
autoWidth := calcWidth(width)
for n, col := range row.all() {
if colWidths[n] != 0 {
col.SetX(x)
col.SetWidth(colWidths[n])
x += colWidths[n]
continue
}
col.SetX(x)
col.SetWidth(autoWidth)
x += autoWidth + colSpacing
}
row.Width = width
}
func (row *Compact) Buffer() ui.Buffer {
buf := ui.NewBuffer()
buf.Merge(row.Status.Buffer())
buf.Merge(row.Name.Buffer())
buf.Merge(row.Cid.Buffer())
buf.Merge(row.Cpu.Buffer())
buf.Merge(row.Memory.Buffer())
buf.Merge(row.Net.Buffer())
buf.Merge(row.IO.Buffer())
buf.Merge(row.Pids.Buffer())
return buf
}
func (row *Compact) all() []ui.GridBufferer {
return []ui.GridBufferer{
row.Status,
row.Name,
row.Cid,
row.Cpu,
row.Memory,
row.Net,
row.IO,
row.Pids,
}
}

129
cwidgets/compact/row.go Normal file
View 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") }

View File

@@ -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
}

View File

@@ -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)
} }

View File

@@ -1,35 +1,135 @@
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 { // Column that shows container's meta property i.e. name, id, image tc.
*ui.Par type MetaCol struct {
*TextCol
metaName string
} }
func NewTextCol(s string) *TextCol { func (w *MetaCol) SetMeta(m models.Meta) {
p := ui.NewPar(s) w.setText(m.Get(w.metaName))
}
func NewNameCol() CompactCol {
c := &MetaCol{NewTextCol("NAME"), "name"}
c.fWidth = 30
return c
}
func NewCIDCol() CompactCol {
c := &MetaCol{NewTextCol("CID"), "id"}
c.fWidth = 12
return c
}
func NewImageCol() CompactCol {
return &MetaCol{NewTextCol("IMAGE"), "image"}
}
func NewPortsCol() CompactCol {
return &MetaCol{NewTextCol("PORTS"), "ports"}
}
func NewIpsCol() CompactCol {
return &MetaCol{NewTextCol("IPs"), "IPs"}
}
func NewCreatedCol() CompactCol {
c := &MetaCol{NewTextCol("CREATED"), "created"}
c.fWidth = 19 // Year will be stripped e.g. "Thu Nov 26 07:44:03" without 2020 at end
return c
}
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
} }

View File

@@ -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

View File

@@ -1,35 +0,0 @@
package expanded
import (
ui "github.com/gizak/termui"
)
var displayInfo = []string{"id", "name", "image", "state"}
type Info struct {
*ui.Table
data map[string]string
}
func NewInfo(id string) *Info {
p := ui.NewTable()
p.Height = 4
p.Width = colWidth[0]
p.FgColor = ui.ThemeAttr("par.text.fg")
p.Separator = false
i := &Info{p, make(map[string]string)}
i.Set("id", id)
return i
}
func (w *Info) Set(k, v string) {
w.data[k] = v
// rebuild rows
w.Rows = [][]string{}
for _, k := range displayInfo {
if v, ok := w.data[k]; ok {
w.Rows = append(w.Rows, []string{k, v})
}
}
w.Height = len(w.Rows) + 2
}

View File

@@ -2,12 +2,20 @@ package cwidgets
import ( import (
"github.com/bcicen/ctop/logging" "github.com/bcicen/ctop/logging"
"github.com/bcicen/ctop/metrics" "github.com/bcicen/ctop/models"
) )
var log = logging.Init() var log = logging.Init()
type WidgetUpdater interface { type WidgetUpdater interface {
SetMeta(string, string) SetMeta(models.Meta)
SetMetrics(metrics.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) {}

View File

@@ -1,4 +1,4 @@
package expanded package single
import ( import (
ui "github.com/gizak/termui" ui "github.com/gizak/termui"

39
cwidgets/single/env.go Normal file
View File

@@ -0,0 +1,39 @@
package single
import (
ui "github.com/gizak/termui"
"regexp"
"strings"
)
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(allEnvs string) {
envs := strings.Split(allEnvs, ";")
w.Rows = [][]string{}
for _, env := range envs {
match := envPattern.FindStringSubmatch(env)
key := match[1]
value := match[2]
w.data[key] = value
w.Rows = append(w.Rows, mkInfoRows(key, value)...)
}
w.Height = len(w.Rows) + 2
}

View File

@@ -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

57
cwidgets/single/info.go Normal file
View File

@@ -0,0 +1,57 @@
package single
import (
"strings"
ui "github.com/gizak/termui"
)
var displayInfo = []string{"id", "name", "image", "ports", "IPs", "state", "created", "health"}
type Info struct {
*ui.Table
data map[string]string
}
func NewInfo() *Info {
p := ui.NewTable()
p.Height = 4
p.Width = colWidth[0]
p.FgColor = ui.ThemeAttr("par.text.fg")
p.Separator = false
i := &Info{p, make(map[string]string)}
return i
}
func (w *Info) Set(k, v string) {
w.data[k] = v
// rebuild rows
w.Rows = [][]string{}
for _, k := range displayInfo {
if v, ok := w.data[k]; ok {
w.Rows = append(w.Rows, mkInfoRows(k, v)...)
}
}
w.Height = len(w.Rows) + 2
}
// Build row(s) from a key and value string
func mkInfoRows(k, v string) (rows [][]string) {
lines := strings.Split(v, "\n")
// initial row with field name
rows = append(rows, []string{k, lines[0]})
// append any additional lines in separate row
if len(lines) > 1 {
for _, line := range lines[1:] {
if line != "" {
rows = append(rows, []string{"", line})
}
}
}
return rows
}

View File

@@ -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)
} }

83
cwidgets/single/logs.go Normal file
View File

@@ -0,0 +1,83 @@
package single
import (
"time"
"github.com/bcicen/ctop/models"
ui "github.com/gizak/termui"
)
type LogLines struct {
ts []time.Time
data []string
}
func NewLogLines(max int) *LogLines {
ll := &LogLines{
ts: make([]time.Time, max),
data: make([]string, max),
}
return ll
}
func (ll *LogLines) tail(n int) []string {
lines := make([]string, n)
for i := 0; i < n; i++ {
lines = append(lines, ll.data[len(ll.data)-i])
}
return lines
}
func (ll *LogLines) getLines(start, end int) []string {
if end < 0 {
return ll.data[start:]
}
return ll.data[start:end]
}
func (ll *LogLines) add(l models.Log) {
if len(ll.data) == cap(ll.data) {
ll.data = append(ll.data[:0], ll.data[1:]...)
ll.ts = append(ll.ts[:0], ll.ts[1:]...)
}
ll.ts = append(ll.ts, l.Timestamp)
ll.data = append(ll.data, l.Message)
log.Debugf("recorded log line: %v", l)
}
type Logs struct {
*ui.List
lines *LogLines
}
func NewLogs(stream chan models.Log) *Logs {
p := ui.NewList()
p.Y = ui.TermHeight() / 2
p.X = 0
p.Height = ui.TermHeight() - p.Y
p.Width = ui.TermWidth()
//p.Overflow = "wrap"
p.ItemFgColor = ui.ThemeAttr("par.text.fg")
i := &Logs{p, NewLogLines(4098)}
go func() {
for line := range stream {
i.lines.add(line)
ui.Render(i)
}
}()
return i
}
func (w *Logs) Align() {
w.X = colWidth[0]
w.List.Align()
}
func (w *Logs) Buffer() ui.Buffer {
maxLines := w.Height - 2
offset := len(w.lines.data) - maxLines
w.Items = w.lines.getLines(offset, -1)
return w.List.Buffer()
}
// number of rows a line will occupy at current panel width
func (w *Logs) lineHeight(s string) int { return (len(s) / w.InnerWidth()) + 1 }

View File

@@ -1,8 +1,8 @@
package expanded package single
import ( import (
"github.com/bcicen/ctop/logging" "github.com/bcicen/ctop/logging"
"github.com/bcicen/ctop/metrics" "github.com/bcicen/ctop/models"
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
) )
@@ -12,31 +12,30 @@ 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() *Single {
if len(id) > 12 { return &Single{
id = id[:12] Info: NewInfo(),
}
return &Expanded{
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 +43,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 +51,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(v)
} else {
e.Info.Set(k, v)
}
}
}
func (e *Expanded) SetMetrics(m metrics.Metrics) { func (e *Single) SetMetrics(m models.Metrics) {
e.Cpu.Update(m.CPUUtil) e.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 +99,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 +111,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,
} }
} }

View File

@@ -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
} }

View File

@@ -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)
} }

View File

@@ -1,48 +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)
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))
}
nf := float64(n) / gb
return fmt.Sprintf("%sG", 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
} }

View File

@@ -3,11 +3,19 @@ package main
import ( import (
"fmt" "fmt"
"reflect" "reflect"
"runtime"
"github.com/bcicen/ctop/container"
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
) )
var mstats = &runtime.MemStats{}
func logEvent(e ui.Event) { func logEvent(e ui.Event) {
// skip timer events e.g. /timer/1s
if e.From == "timer" {
return
}
var s string var s string
s += fmt.Sprintf("Type=%s", quote(e.Type)) s += fmt.Sprintf("Type=%s", quote(e.Type))
s += fmt.Sprintf(" Path=%s", quote(e.Path)) s += fmt.Sprintf(" Path=%s", quote(e.Path))
@@ -18,8 +26,24 @@ func logEvent(e ui.Event) {
log.Debugf("new event: %s", s) log.Debugf("new event: %s", s)
} }
func runtimeStats() {
var msg string
msg += fmt.Sprintf("cgo calls=%v", runtime.NumCgoCall())
msg += fmt.Sprintf(" routines=%v", runtime.NumGoroutine())
runtime.ReadMemStats(mstats)
msg += fmt.Sprintf(" numgc=%v", mstats.NumGC)
msg += fmt.Sprintf(" alloc=%v", mstats.Alloc)
log.Debugf("runtime: %v", msg)
}
func runtimeStack() {
buf := make([]byte, 32768)
buf = buf[:runtime.Stack(buf, true)]
log.Infof(fmt.Sprintf("stack:\n%v", string(buf)))
}
// log container, metrics, and widget state // log container, metrics, and widget state
func dumpContainer(c *Container) { func dumpContainer(c *container.Container) {
msg := fmt.Sprintf("logging state for container: %s\n", c.Id) msg := fmt.Sprintf("logging state for container: %s\n", c.Id)
for k, v := range c.Meta { for k, v := range c.Meta {
msg += fmt.Sprintf("Meta.%s = %s\n", k, v) msg += fmt.Sprintf("Meta.%s = %s\n", k, v)

View File

@@ -1,156 +0,0 @@
package main
import (
"sort"
"strings"
"sync"
"github.com/bcicen/ctop/metrics"
"github.com/fsouza/go-dockerclient"
)
type ContainerSource interface {
All() Containers
Get(string) (*Container, bool)
}
type DockerContainerSource struct {
client *docker.Client
containers map[string]*Container
needsRefresh chan string // container IDs requiring refresh
lock sync.RWMutex
}
func NewDockerContainerSource() *DockerContainerSource {
// init docker client
client, err := docker.NewClientFromEnv()
if err != nil {
panic(err)
}
cm := &DockerContainerSource{
client: client,
containers: make(map[string]*Container),
needsRefresh: make(chan string, 60),
lock: sync.RWMutex{},
}
go cm.Loop()
cm.refreshAll()
go cm.watchEvents()
return cm
}
// Docker events watcher
func (cm *DockerContainerSource) watchEvents() {
log.Info("docker event listener starting")
events := make(chan *docker.APIEvents)
cm.client.AddEventListener(events)
for e := range events {
if e.Type != "container" {
continue
}
switch e.Action {
case "start", "die", "pause", "unpause":
log.Debugf("handling docker event: action=%s id=%s", e.Action, e.ID)
cm.needsRefresh <- e.ID
case "destroy":
log.Debugf("handling docker event: action=%s id=%s", e.Action, e.ID)
cm.delByID(e.ID)
}
}
}
func (cm *DockerContainerSource) refresh(c *Container) {
insp := cm.inspect(c.Id)
// remove container if no longer exists
if insp == nil {
cm.delByID(c.Id)
return
}
c.SetMeta("name", shortName(insp.Name))
c.SetMeta("image", insp.Config.Image)
c.SetMeta("created", insp.Created.Format("Mon Jan 2 15:04:05 2006"))
c.SetState(insp.State.Status)
}
func (cm *DockerContainerSource) inspect(id string) *docker.Container {
c, err := cm.client.InspectContainer(id)
if err != nil {
if _, ok := err.(*docker.NoSuchContainer); ok == false {
log.Errorf(err.Error())
}
}
return c
}
// Mark all container IDs for refresh
func (cm *DockerContainerSource) refreshAll() {
opts := docker.ListContainersOptions{All: true}
allContainers, err := cm.client.ListContainers(opts)
if err != nil {
panic(err)
}
for _, i := range allContainers {
c := cm.MustGet(i.ID)
c.SetMeta("name", shortName(i.Names[0]))
c.SetState(i.State)
cm.needsRefresh <- c.Id
}
}
func (cm *DockerContainerSource) Loop() {
for id := range cm.needsRefresh {
c := cm.MustGet(id)
cm.refresh(c)
}
}
// Get a single container, creating one anew if not existing
func (cm *DockerContainerSource) MustGet(id string) *Container {
c, ok := cm.Get(id)
// append container struct for new containers
if !ok {
// create collector
collector := metrics.NewDocker(cm.client, id)
// create container
c = NewContainer(id, collector)
cm.lock.Lock()
cm.containers[id] = c
cm.lock.Unlock()
}
return c
}
// Get a single container, by ID
func (cm *DockerContainerSource) Get(id string) (*Container, bool) {
cm.lock.Lock()
c, ok := cm.containers[id]
cm.lock.Unlock()
return c, ok
}
// Remove containers by ID
func (cm *DockerContainerSource) delByID(id string) {
cm.lock.Lock()
delete(cm.containers, id)
cm.lock.Unlock()
log.Infof("removed dead container: %s", id)
}
// Return array of all containers, sorted by field
func (cm *DockerContainerSource) All() (containers Containers) {
cm.lock.Lock()
for _, c := range cm.containers {
containers = append(containers, c)
}
cm.lock.Unlock()
sort.Sort(containers)
containers.Filter()
return containers
}
// use primary container name
func shortName(name string) string {
return strings.Replace(name, "/", "", 1)
}

85
glide.lock generated
View File

@@ -1,85 +0,0 @@
hash: c13011881e895378f374b68596a59a0ea6def372b4f5239b2d8aa342eaa46a4b
updated: 2017-03-12T09:53:35.212073637+07:00
imports:
- name: github.com/Azure/go-ansiterm
version: fa152c58bc15761d0200cb75fe958b89a9d4888e
subpackages:
- winterm
- name: github.com/docker/docker
version: ce07fb6b0f1b8765b92022e45f96bd4349812e06
subpackages:
- api/types
- api/types/blkiodev
- api/types/container
- api/types/filters
- api/types/mount
- api/types/network
- api/types/registry
- api/types/strslice
- api/types/swarm
- api/types/versions
- opts
- pkg/archive
- pkg/fileutils
- pkg/homedir
- pkg/idtools
- pkg/ioutils
- pkg/jsonlog
- pkg/jsonmessage
- pkg/longpath
- pkg/pools
- pkg/promise
- pkg/stdcopy
- pkg/system
- pkg/term
- pkg/term/windows
- name: github.com/docker/go-connections
version: a2afab9802043837035592f1c24827fb70766de9
subpackages:
- nat
- name: github.com/docker/go-units
version: 0dadbb0345b35ec7ef35e228dabb8de89a65bf52
- name: github.com/fsouza/go-dockerclient
version: 318513eb1ab27495afbc67f671ba1080513d8aa0
- name: github.com/gizak/termui
version: ea10e6ccee219e572ffad0ac1909f1a17f6db7d6
repo: https://github.com/bcicen/termui
vcs: git
- name: github.com/hashicorp/go-cleanhttp
version: 3573b8b52aa7b37b9358d966a898feb387f62437
- name: github.com/jgautheron/codename-generator
version: 16d037c7cc3c9b552fe4af9828b7338d752dbaf9
- name: github.com/maruel/panicparse
version: 25bcac0d793cf4109483505a0d66e066a3a90a80
subpackages:
- stack
- name: github.com/mattn/go-runewidth
version: 14207d285c6c197daabb5c9793d63e7af9ab2d50
- name: github.com/Microsoft/go-winio
version: fff283ad5116362ca252298cfc9b95828956d85d
- name: github.com/mitchellh/go-wordwrap
version: ad45545899c7b13c020ea92b2072220eefad42b8
- name: github.com/nsf/termbox-go
version: 91bae1bb5fa9ee504905ecbe7043fa30e92feaa3
- name: github.com/nu7hatch/gouuid
version: 179d4d0c4d8d407a32af483c2354df1d2c91e6c3
- name: github.com/op/go-logging
version: b2cb9fa56473e98db8caba80237377e83fe44db5
- name: github.com/opencontainers/runc
version: 31980a53ae7887b2c8f8715d13c3eb486c27b6cf
subpackages:
- libcontainer/system
- libcontainer/user
- name: github.com/Sirupsen/logrus
version: 1deb2db2a6fff8a35532079061b903c3a25eed52
- name: golang.org/x/net
version: a6577fac2d73be281a500b310739095313165611
subpackages:
- context
- context/ctxhttp
- name: golang.org/x/sys
version: 99f16d856c9836c42d24e7ab64ea72916925fa97
subpackages:
- unix
- windows
testImports: []

View File

@@ -1,11 +0,0 @@
package: github.com/bcicen/ctop
import:
- package: github.com/fsouza/go-dockerclient
- package: github.com/gizak/termui
version: barchart-numfmt
repo: https://github.com/bcicen/termui
vcs: git
- package: github.com/jgautheron/codename-generator
- package: github.com/nu7hatch/gouuid
- package: github.com/op/go-logging
version: ^1.0.0

21
go.mod Normal file
View 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

321
go.sum Normal file
View File

@@ -0,0 +1,321 @@
bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898 h1:SC+c6A1qTFstO9qmB86mPV2IpYme/2ZoEQ0hrP+wo+Q=
bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ=
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 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
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 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f h1:WBZRG4aNOuI15bLRrCgN8fCq8E5Xuty6jGbmSNEvSsU=
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 h1:tSNMc+rJDfmYntojat8lljbt1mgKNpTxUZJsSzJ9Y1s=
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 h1:PUD50EuOMkXVcpBIA/R95d56duJR9VxhwncsFbNnxW4=
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3 h1:esQOJREg8nw8aXj6uCN5dfW5cKUBiEJ/+nni1Q/D/sw=
github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0=
github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de h1:dlfGmNcE3jDAecLqwKPMNX6nk2qh1c1Vg1/YTzpOOF4=
github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o=
github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd h1:JNn81o/xG+8NEo3bC/vx9pbi/g2WI8mtP2/nXzu297Y=
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 h1:qk/FSDDxo05wdJH28W+p5yivv7LuLYLRXPPD8KQCtZs=
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 h1:rEvIZUSZ3fx39WIi3JkQqQBitGwpELBIYWeBVh6wn+E=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
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 h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=
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 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
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 h1:reN85Pxc5larApoH1keMBiu2GWtPqXQ1nc9gx+jOU+E=
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
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 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
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 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo=
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME=
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 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7 h1:hhvfGDVThBnd4kYisSFmYuHYeUhglxcwag7FhVPH9zM=
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 h1:GQkkv3XSnxhAMjdq2wLfEnptEVr+2BNvmHizILHn+d4=
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 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
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 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
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 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4=
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 h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA=
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 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0=
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 h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
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 h1:1mMox4TgefDwqluYCv677yNXwlfTkija4owZve/jr78=
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 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
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 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo=
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 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0=
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
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 h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

146
grid.go
View File

@@ -2,10 +2,48 @@ package main
import ( import (
"github.com/bcicen/ctop/config" "github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/cwidgets/expanded" "github.com/bcicen/ctop/cwidgets/single"
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()
@@ -17,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 {
@@ -34,12 +73,17 @@ func RedrawRows(clr bool) {
ui.Render(cGrid) ui.Render(cGrid)
} }
func ExpandView(c *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.SetUpdater(ex) c.SetUpdater(ex)
ex.Align() ex.Align()
@@ -58,27 +102,40 @@ func ExpandView(c *Container) {
ui.Loop() ui.Loop()
c.SetUpdater(c.Widgets) c.SetUpdater(c.Widgets)
return nil
} }
func RefreshDisplay() { func RefreshDisplay() error {
needsClear := cursor.RefreshContainers() // skip display refresh during scroll
if !cursor.isScrolling {
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)
HandleKeys("up", cursor.Up) HandleKeys("up", cursor.Up)
HandleKeys("down", cursor.Down) HandleKeys("down", cursor.Down)
HandleKeys("pgup", cursor.PgUp)
HandleKeys("pgdown", cursor.PgDown)
HandleKeys("exit", ui.StopLoop) HandleKeys("exit", ui.StopLoop)
HandleKeys("help", func() { HandleKeys("help", func() {
menu = HelpMenu menu = HelpMenu
@@ -86,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())
@@ -111,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())
@@ -125,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
View 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!"

View File

@@ -14,9 +14,18 @@ var keyMap = map[string][]string{
"/sys/kbd/<down>", "/sys/kbd/<down>",
"/sys/kbd/j", "/sys/kbd/j",
}, },
"pgup": []string{
"/sys/kbd/<previous>",
"/sys/kbd/C-<up>",
},
"pgdown": []string{
"/sys/kbd/<next>",
"/sys/kbd/C-<down>",
},
"exit": []string{ "exit": []string{
"/sys/kbd/q", "/sys/kbd/q",
"/sys/kbd/C-c", "/sys/kbd/C-c",
"/sys/kbd/<escape>",
}, },
"help": []string{ "help": []string{
"/sys/kbd/h", "/sys/kbd/h",

View File

@@ -1,6 +1,7 @@
package logging package logging
import ( import (
"fmt"
"os" "os"
"time" "time"
@@ -20,11 +21,37 @@ var (
) )
) )
type statusMsg struct {
Text string
IsError bool
}
type CTopLogger struct { type CTopLogger struct {
*logging.Logger *logging.Logger
backend *logging.MemoryBackend backend *logging.MemoryBackend
logFile *os.File
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,17 +59,37 @@ func Init() *CTopLogger {
Log = &CTopLogger{ Log = &CTopLogger{
logging.MustGetLogger("ctop"), logging.MustGetLogger("ctop"),
logging.NewMemoryBackend(size), logging.NewMemoryBackend(size),
nil,
[]statusMsg{},
} }
if debugMode() { debugMode := debugMode()
if debugMode {
level = logging.DEBUG level = logging.DEBUG
StartServer()
} }
backendLvl := logging.AddModuleLevel(Log.backend) backendLvl := logging.AddModuleLevel(Log.backend)
backendLvl.SetLevel(level, "") backendLvl.SetLevel(level, "")
logFilePath := debugModeFile()
if logFilePath == "" {
logging.SetBackend(backendLvl) logging.SetBackend(backendLvl)
} else {
logFile, err := os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666)
if err != nil {
logging.SetBackend(backendLvl)
Log.Error("Unable to create log file: %s", err.Error())
} else {
backendFile := logging.NewLogBackend(logFile, "", 0)
backendFileLvl := logging.AddModuleLevel(backendFile)
backendFileLvl.SetLevel(level, "")
logging.SetBackend(backendLvl, backendFileLvl)
Log.logFile = logFile
}
}
if debugMode {
StartServer()
}
Log.Notice("logger initialized") Log.Notice("logger initialized")
} }
return Log return Log
@@ -75,7 +122,12 @@ func (log *CTopLogger) tail() chan string {
func (log *CTopLogger) Exit() { func (log *CTopLogger) Exit() {
exited = true exited = true
if log.logFile != nil {
_ = log.logFile.Close()
}
StopServer() StopServer()
} }
func debugMode() bool { return os.Getenv("CTOP_DEBUG") == "1" } func debugMode() bool { return os.Getenv("CTOP_DEBUG") == "1" }
func debugModeTCP() bool { return os.Getenv("CTOP_DEBUG_TCP") == "1" }
func debugModeFile() string { return os.Getenv("CTOP_DEBUG_FILE") }

View File

@@ -2,12 +2,14 @@ package logging
import ( import (
"fmt" "fmt"
"io"
"net" "net"
"sync" "sync"
) )
const ( const (
path = "/tmp/ctop.sock" socketPath = "./ctop.sock"
socketAddr = "0.0.0.0:9000"
) )
var server struct { var server struct {
@@ -16,7 +18,13 @@ var server struct {
} }
func getListener() net.Listener { func getListener() net.Listener {
ln, err := net.Listen("unix", path) var ln net.Listener
var err error
if debugModeTCP() {
ln, err = net.Listen("tcp", socketAddr)
} else {
ln, err = net.Listen("unix", socketPath)
}
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -49,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"))
} }

70
main.go
View File

@@ -4,39 +4,52 @@ import (
"flag" "flag"
"fmt" "fmt"
"os" "os"
"runtime"
"strings"
"github.com/bcicen/ctop/config" "github.com/bcicen/ctop/config"
"github.com/bcicen/ctop/connector"
"github.com/bcicen/ctop/container"
"github.com/bcicen/ctop/cwidgets/compact" "github.com/bcicen/ctop/cwidgets/compact"
"github.com/bcicen/ctop/logging" "github.com/bcicen/ctop/logging"
"github.com/bcicen/ctop/widgets" "github.com/bcicen/ctop/widgets"
ui "github.com/gizak/termui" ui "github.com/gizak/termui"
tm "github.com/nsf/termbox-go"
) )
var ( var (
build = "none" build = "none"
version = "dev-build" version = "dev-build"
goVersion = runtime.Version()
log *logging.CTopLogger log *logging.CTopLogger
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)
) )
func main() { 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")
invertFlag = flag.Bool("i", false, "invert default colors")
connectorFlag = flag.String("connector", "docker", "container connector to use")
)
flag.Parse() flag.Parse()
if *versionFlag { if *versionFlag {
printVersion() fmt.Println(versionStr)
os.Exit(0) os.Exit(0)
} }
@@ -48,8 +61,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,26 +93,39 @@ func main() {
if err := ui.Init(); err != nil { if err := ui.Init(); err != nil {
panic(err) panic(err)
} }
defer ui.Close() tm.SetInputMode(tm.InputAlt)
defer Shutdown()
// init grid, cursor, header // init grid, cursor, header
cursor = NewGridCursor() cSuper, err := connector.ByName(*connectorFlag)
if err != nil {
panic(err)
}
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()
if exit { if exit {
log.Notice("shutting down")
log.Exit()
return return
} }
} }
} }
func Shutdown() {
log.Notice("shutting down")
log.Exit()
if tm.IsInit {
ui.Close()
}
}
// ensure a given sort field is valid // ensure a given sort field is valid
func validSort(s string) { func validSort(s string) {
if _, ok := Sorters[s]; !ok { if _, ok := container.Sorters[s]; !ok {
fmt.Printf("invalid sort field: %s\n", s) fmt.Printf("invalid sort field: %s\n", s)
os.Exit(1) os.Exit(1)
} }
@@ -104,13 +133,14 @@ func validSort(s string) {
func panicExit() { func panicExit() {
if r := recover(); r != nil { if r := recover(); r != nil {
ui.Clear() Shutdown()
fmt.Printf("panic: %s\n", r) panic(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]
@@ -120,8 +150,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(), ", "))
func printVersion() {
fmt.Printf("ctop version %v, build %v\n", version, build)
} }

396
menus.go
View File

@@ -1,23 +1,38 @@
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/widgets" "github.com/bcicen/ctop/widgets"
"github.com/bcicen/ctop/widgets/menu" "github.com/bcicen/ctop/widgets/menu"
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()
@@ -25,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()
@@ -62,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()
@@ -74,7 +94,7 @@ func SortMenu() {
m.SortItems = true m.SortItems = true
m.BorderLabel = "Sort Field" m.BorderLabel = "Sort Field"
for _, field := range SortFields() { for _, field := range container.SortFields() {
m.AddItems(menu.Item{field, ""}) m.AddItems(menu.Item{field, ""})
} }
@@ -86,10 +106,362 @@ 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 = "Re-order: <Page Up> / <Page Down>"
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()
// Detect and execute default shell in container.
// Execute Ash shell command: /bin/sh -c
// Reset colors: printf '\e[0m\e[?25h'
// Clear screen
// Run default shell for the user. It's configured in /etc/passwd and looks like root:x:0:0:root:/root:/bin/bash:
// 1. Get current user id: id -un
// 2. Find user's line in /etc/passwd by grep
// 3. Extract default user's shell by cutting seven's column separated by :
// 4. Execute the shell path with eval
if err := c.Exec([]string{"/bin/sh", "-c", "printf '\\e[0m\\e[?25h' && clear && eval `grep ^$(id -un): /etc/passwd | cut -d : -f 7-`"}); err != nil {
log.StatusErr(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) }

View File

@@ -1,45 +0,0 @@
package metrics
import (
"math"
"github.com/bcicen/ctop/logging"
)
var log = logging.Init()
type Metrics struct {
CPUUtil int
NetTx int64
NetRx int64
MemLimit int64
MemPercent int
MemUsage int64
IOBytesRead int64
IOBytesWrite int64
Pids int
}
func NewMetrics() Metrics {
return Metrics{
CPUUtil: -1,
NetTx: -1,
NetRx: -1,
MemUsage: -1,
MemPercent: -1,
IOBytesRead: -1,
IOBytesWrite: -1,
Pids: -1,
}
}
type Collector interface {
Stream() chan Metrics
Running() bool
Start()
Stop()
}
func round(num float64) int {
return int(num + math.Copysign(0.5, num))
}

57
models/main.go Normal file
View File

@@ -0,0 +1,57 @@
package models
import "time"
type Log struct {
Timestamp time.Time
Message string
}
type Meta map[string]string
// NewMeta returns an initialized Meta map.
// An optional series of key, values may be provided to populate the map prior to returning
func NewMeta(kvs ...string) Meta {
m := make(Meta)
var i int
for i < len(kvs)-1 {
m[kvs[i]] = kvs[i+1]
i += 2
}
return m
}
func (m Meta) Get(k string) string {
if s, ok := m[k]; ok {
return s
}
return ""
}
type Metrics struct {
NCpus uint8
CPUUtil int
NetTx int64
NetRx int64
MemLimit int64
MemPercent int
MemUsage int64
IOBytesRead int64
IOBytesWrite int64
Pids int
}
func NewMetrics() Metrics {
return Metrics{
CPUUtil: -1,
NetTx: -1,
NetRx: -1,
MemUsage: -1,
MemPercent: -1,
IOBytesRead: -1,
IOBytesWrite: -1,
Pids: -1,
}
}

60
widgets/error.go Normal file
View 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))
}

View File

@@ -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(),
} }
} }

View File

@@ -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)

View File

@@ -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
View 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
View 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
}

125
widgets/view.go Normal file
View File

@@ -0,0 +1,125 @@
package widgets
import (
ui "github.com/gizak/termui"
"github.com/mattn/go-runewidth"
)
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 + runewidth.RuneWidth(ch)
}
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
View 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))
}
}