cgse-tui 0.26.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cgse_tui-0.26.0/.gitignore +65 -0
- cgse_tui-0.26.0/PKG-INFO +127 -0
- cgse_tui-0.26.0/README.md +103 -0
- cgse_tui-0.26.0/pyproject.toml +81 -0
- cgse_tui-0.26.0/src/cgse_tui/__init__.py +1 -0
- cgse_tui-0.26.0/src/cgse_tui/app.py +175 -0
- cgse_tui-0.26.0/src/cgse_tui/cli.py +32 -0
- cgse_tui-0.26.0/src/cgse_tui/data/__init__.py +1 -0
- cgse_tui-0.26.0/src/cgse_tui/data/actions.py +278 -0
- cgse_tui-0.26.0/src/cgse_tui/data/collector.py +518 -0
- cgse_tui-0.26.0/src/cgse_tui/data/health.py +75 -0
- cgse_tui-0.26.0/src/cgse_tui/data/model.py +156 -0
- cgse_tui-0.26.0/src/cgse_tui/data/monitor.py +78 -0
- cgse_tui-0.26.0/src/cgse_tui/data/report.py +215 -0
- cgse_tui-0.26.0/src/cgse_tui/data/services.py +58 -0
- cgse_tui-0.26.0/src/cgse_tui/data/setup.py +80 -0
- cgse_tui-0.26.0/src/cgse_tui/details.py +377 -0
- cgse_tui-0.26.0/src/cgse_tui/logs.py +109 -0
- cgse_tui-0.26.0/src/cgse_tui/plugins.py +219 -0
- cgse_tui-0.26.0/src/cgse_tui/screens/__init__.py +1 -0
- cgse_tui-0.26.0/src/cgse_tui/screens/action_select.py +160 -0
- cgse_tui-0.26.0/src/cgse_tui/screens/column_select.py +95 -0
- cgse_tui-0.26.0/src/cgse_tui/screens/columns.py +328 -0
- cgse_tui-0.26.0/src/cgse_tui/screens/detail.py +481 -0
- cgse_tui-0.26.0/src/cgse_tui/screens/files_screen.py +330 -0
- cgse_tui-0.26.0/src/cgse_tui/screens/help.py +250 -0
- cgse_tui-0.26.0/src/cgse_tui/screens/log_filter.py +180 -0
- cgse_tui-0.26.0/src/cgse_tui/screens/log_screen.py +720 -0
- cgse_tui-0.26.0/src/cgse_tui/screens/logtail.py +188 -0
- cgse_tui-0.26.0/src/cgse_tui/screens/logview.py +80 -0
- cgse_tui-0.26.0/src/cgse_tui/screens/overview.py +202 -0
- cgse_tui-0.26.0/src/cgse_tui/screens/panels.py +330 -0
- cgse_tui-0.26.0/src/cgse_tui/screens/procman.py +71 -0
- cgse_tui-0.26.0/src/cgse_tui/screens/record_view.py +217 -0
- cgse_tui-0.26.0/src/cgse_tui/screens/registry.py +99 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Python versions and environment
|
|
2
|
+
|
|
3
|
+
__pycache__
|
|
4
|
+
.python-version
|
|
5
|
+
.envrc
|
|
6
|
+
|
|
7
|
+
# Build systems
|
|
8
|
+
|
|
9
|
+
build
|
|
10
|
+
dist
|
|
11
|
+
**/*.egg-info
|
|
12
|
+
|
|
13
|
+
# Apple specific
|
|
14
|
+
|
|
15
|
+
.DS_Store
|
|
16
|
+
|
|
17
|
+
# Unit testing
|
|
18
|
+
|
|
19
|
+
.pytest_cache
|
|
20
|
+
.coverage
|
|
21
|
+
.coverage.*
|
|
22
|
+
.nox
|
|
23
|
+
htmlcov
|
|
24
|
+
libs/cgse-core/temperature.csv
|
|
25
|
+
|
|
26
|
+
# Prototypes
|
|
27
|
+
|
|
28
|
+
**/prototypes/typed_serialization
|
|
29
|
+
|
|
30
|
+
# dotenv, ignore all but the example file
|
|
31
|
+
|
|
32
|
+
.env
|
|
33
|
+
.env.*
|
|
34
|
+
!.env.example
|
|
35
|
+
|
|
36
|
+
# Virtual environments
|
|
37
|
+
|
|
38
|
+
.venv
|
|
39
|
+
venv
|
|
40
|
+
|
|
41
|
+
# PyCharm IDE
|
|
42
|
+
|
|
43
|
+
.idea
|
|
44
|
+
|
|
45
|
+
# VSCode IDE
|
|
46
|
+
|
|
47
|
+
.vscode
|
|
48
|
+
*.code-workspace
|
|
49
|
+
|
|
50
|
+
# MKDOCS documentation site
|
|
51
|
+
|
|
52
|
+
/site
|
|
53
|
+
|
|
54
|
+
# Packaging
|
|
55
|
+
|
|
56
|
+
uv.lock
|
|
57
|
+
libs/test_service_registry.db
|
|
58
|
+
libs/cgse-core/metrics.duckdb
|
|
59
|
+
install_influxdb3.sh
|
|
60
|
+
.pylintrc
|
|
61
|
+
|
|
62
|
+
# CGSE Specific
|
|
63
|
+
|
|
64
|
+
.migrate_influx_to_questdb.state.json
|
|
65
|
+
service_registry.db*
|
cgse_tui-0.26.0/PKG-INFO
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: cgse-tui
|
|
3
|
+
Version: 0.26.0
|
|
4
|
+
Summary: Terminal user interface with the status of the CGSE core services and device control servers
|
|
5
|
+
Project-URL: Homepage, https://github.com/IvS-KULeuven/cgse/tree/main/projects/generic/cgse-tui
|
|
6
|
+
Project-URL: Repository, https://github.com/IvS-KULeuven/cgse
|
|
7
|
+
Project-URL: Releases, https://github.com/IvS-KULeuven/cgse/releases
|
|
8
|
+
Project-URL: Changelog, https://github.com/IvS-KULeuven/cgse/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Documentation, https://ivs-kuleuven.github.io/cgse/
|
|
10
|
+
Author: IvS KU Leuven
|
|
11
|
+
Maintainer-email: Rik Huygen <rik.huygen@kuleuven.be>
|
|
12
|
+
License-Expression: MIT
|
|
13
|
+
Keywords: CGSE,Common-EGSE,hardware testing,software framework,textual
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Requires-Dist: cgse-common
|
|
16
|
+
Requires-Dist: cgse-core
|
|
17
|
+
Requires-Dist: textual>=2.0.0
|
|
18
|
+
Requires-Dist: typer
|
|
19
|
+
Provides-Extra: test
|
|
20
|
+
Requires-Dist: pytest; extra == 'test'
|
|
21
|
+
Requires-Dist: pytest-cov; extra == 'test'
|
|
22
|
+
Requires-Dist: pytest-mock; extra == 'test'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# CGSE TUI
|
|
26
|
+
|
|
27
|
+
A terminal user interface, built with [Textual](https://textual.textualize.io/), that shows the status of the CGSE
|
|
28
|
+
core services and device control servers.
|
|
29
|
+
|
|
30
|
+
Start it with `cgse tui` or `cgse_tui`.
|
|
31
|
+
|
|
32
|
+
The design and the status information it uses are described in `docs/dev_guide/tui_status_inventory.md`:
|
|
33
|
+
|
|
34
|
+
- the services are found in the service registry;
|
|
35
|
+
- control servers are followed live on their monitoring port, and asked for their status report when they appear or
|
|
36
|
+
are selected;
|
|
37
|
+
- the registry, logger, notification hub and metrics hub are asked for their own status.
|
|
38
|
+
|
|
39
|
+
The first version is read-only: an overview of all services and a detail screen with the status report of the
|
|
40
|
+
selected service.
|
|
41
|
+
|
|
42
|
+
Keys in the overview:
|
|
43
|
+
|
|
44
|
+
- `enter`: show the status report of the selected service;
|
|
45
|
+
- `r`: refresh, poll the registry and request the status of all services now (otherwise the status report of a
|
|
46
|
+
control server, with the device connection, is requested every 10 s);
|
|
47
|
+
- `a`: start, stop or restart the selected service (also in the detail screen), see below;
|
|
48
|
+
- `o`: select the columns of the overview (in the dialog: `space` toggles a column, `a` all, `n` none, `d` defaults,
|
|
49
|
+
`esc` or `o` closes it);
|
|
50
|
+
- `l`: show or hide the log pane; with the focus in the log pane (`tab`), `m` makes it fill the screen and back
|
|
51
|
+
(`esc` too), and `w` wraps the long lines and back. The log pane follows the new messages while it's at the end;
|
|
52
|
+
scrolled up, it stays at the same lines (`end` follows again);
|
|
53
|
+
- `L`: the log screen, also from the detail screen of the logger or with `cgse tui --logs`: the log messages of a time
|
|
54
|
+
window (the latest hour at first), read by the logger from its log files with its `history` command. `[` and `]`
|
|
55
|
+
move the window by its length, `p` and `n` by a day (yesterday), `-` and `+` make it shorter or longer (5 min to 1
|
|
56
|
+
day), `g` goes to a time, `t` to the latest window. The latest window is live: new messages are added every 2 s with
|
|
57
|
+
the `tail` command and the window moves with the time; the view follows while it's at the end (`end`). The tree at
|
|
58
|
+
the left has the loggers of the window with their number of messages; `space` selects a logger and those below it,
|
|
59
|
+
only their messages are shown. Below it the processes that logged the messages, `space` selects a process in the
|
|
60
|
+
same way (with both selected, a message must be of both); `c` clears the selection. `f`, `d` and `w` filter, hide
|
|
61
|
+
the DEBUG messages and wrap, as in the log panel of the logger;
|
|
62
|
+
- `F1`: help, on every screen (from a detail screen or the log screen it opens at the part about that screen);
|
|
63
|
+
- `ctrl+q`: quit.
|
|
64
|
+
|
|
65
|
+
The overview also shows the device drivers of the project: every node of the Setup (loaded by the configuration
|
|
66
|
+
manager) with a `cgse_object`, e.g. `gse.DAQ6510.cgse_object: daq6510`, with its `device_name` and optional
|
|
67
|
+
`device_id`. A device that runs is shown as its service in the registry; one that doesn't run is shown dimmed, with an
|
|
68
|
+
empty frame, and can be started with `a` (`cgse <object> start [<device_id>]`). See "Device drivers in the Setup" in
|
|
69
|
+
`docs/libs/cgse-common/setup.md`.
|
|
70
|
+
|
|
71
|
+
The detail screen shows the selected service in panels, updated every second:
|
|
72
|
+
|
|
73
|
+
- Service: the registration in the service registry and the endpoints (Service, Process and Device have the same
|
|
74
|
+
height);
|
|
75
|
+
- Process: PID, uptime, CPU and memory. CPU % and memory are shown in orange (warning) or red (critical) when the
|
|
76
|
+
process uses too much CPU or its memory keeps growing, with the reason below, see "CPU and memory warnings";
|
|
77
|
+
- Device: the connection and simulator mode, only for services that control a device;
|
|
78
|
+
- Details: the service specific status. For a control server these are the `details` of its status report,
|
|
79
|
+
updated with the live values of its monitoring messages (the border says `live`), for the registry, logger and
|
|
80
|
+
hubs their own status. The package of a device can show its details in its own way with a plugin (e.g. the
|
|
81
|
+
positions of the hexapods). The TUI has its own panels for the core services: the disk usage of the Storage
|
|
82
|
+
Manager, the observation and the whole Setup of the Configuration Manager, the messages per second, the queue
|
|
83
|
+
and the database of the Metrics Hub, the events of the Notification Hub, and the log file, the messages by level
|
|
84
|
+
and the last warning and error of the logger. Otherwise the details are shown as a tree, see "Details in the TUI"
|
|
85
|
+
in `docs/dev_guide/plugins.md`;
|
|
86
|
+
- Registrations: only for the service registry, all registered services; `enter` shows the details of the selected
|
|
87
|
+
service, `esc` goes back.
|
|
88
|
+
- Log: only for the logger, the last log lines, coloured by level, filling the lower part of the screen; new lines are
|
|
89
|
+
requested every 2 s with the `tail` command of the logger (a logger of an older version doesn't have it). The
|
|
90
|
+
lines follow the new messages while the view is at the end; scrolled up, the view stays at the same lines (`end`
|
|
91
|
+
follows again). `m` makes the panel fill the screen and back, `f` filters the lines on levels, process, logger or
|
|
92
|
+
module, words in the message, and a time range, and hides or dims the other lines; `d` hides the DEBUG lines, `w`
|
|
93
|
+
wraps the long lines. The panel keeps the last 1000 messages (the logger keeps 10 000 in memory), the filter applies
|
|
94
|
+
to those;
|
|
95
|
+
- Core services: only for the Process Manager, the core services it manages with their state in the TUI; `enter`
|
|
96
|
+
shows the details of the selected service, `esc` goes back.
|
|
97
|
+
|
|
98
|
+
CPU and memory warnings: the TUI judges the process of every service, and shows the CPU % and memory in orange or red,
|
|
99
|
+
also in the overview, and writes a message in the log pane when this changes:
|
|
100
|
+
|
|
101
|
+
- CPU: the average CPU % over the last minute, a warning from 50 %, critical from 90 %;
|
|
102
|
+
- memory: the growth of the memory, the RSS plus what is swapped out, over the last 10 minutes, a warning from 10 %,
|
|
103
|
+
critical from 25 %. It's judged when there are samples of at least 5 minutes, so a process that just started isn't
|
|
104
|
+
taken for a leak. The swapped-out memory counts because leaked memory ends up in swap: the RSS alone can go down
|
|
105
|
+
while a process leaks. A service of an older version doesn't report its swap, then only the RSS counts.
|
|
106
|
+
|
|
107
|
+
The limits are in `cgse_tui/data/health.py`.
|
|
108
|
+
|
|
109
|
+
Panels are dimmed when the service is stale or not running. Keys in the detail screen: `r` requests the status report
|
|
110
|
+
now (otherwise every 10 s), `a` starts, stops or restarts the service, `v` shows the raw status report, `m`, `f` and `d`
|
|
111
|
+
maximize and filter the log lines of the logger and hide its DEBUG lines, `esc` goes back to the overview.
|
|
112
|
+
|
|
113
|
+
Start, stop and restart run the `cgse <object> start|stop` command of the service, with its device identifier, e.g.
|
|
114
|
+
`cgse puna stop PUNA_01`. The dialog shows the commands; selecting an action is the confirmation. A running service
|
|
115
|
+
can be stopped or restarted, a core service that is not running can be started. With the simulator checkbox, the
|
|
116
|
+
service is started with its simulator: a simulator that is a separate process (e.g. DAQ6510, TVAC) is started first
|
|
117
|
+
with `cgse <object> start-sim` when it isn't running, other drivers are started with `--sim`. The checkbox is checked
|
|
118
|
+
when the service runs with its simulator now. With the verbose debug checkbox, the service (and its simulator) is
|
|
119
|
+
started with `VERBOSE_DEBUG=1`, otherwise without it. The progress and the result are written to the log pane: the
|
|
120
|
+
TUI waits up to 30 s until a started service is registered, or a stopped service is no longer registered. The commands
|
|
121
|
+
run in their own session, so the services started from the TUI don't belong to the terminal of the TUI.
|
|
122
|
+
|
|
123
|
+
## Latest release
|
|
124
|
+
|
|
125
|
+
- PyPI: https://pypi.org/project/cgse-tui/
|
|
126
|
+
- GitHub releases: https://github.com/IvS-KULeuven/cgse/releases
|
|
127
|
+
- Changelog: https://github.com/IvS-KULeuven/cgse/blob/main/CHANGELOG.md
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# CGSE TUI
|
|
2
|
+
|
|
3
|
+
A terminal user interface, built with [Textual](https://textual.textualize.io/), that shows the status of the CGSE
|
|
4
|
+
core services and device control servers.
|
|
5
|
+
|
|
6
|
+
Start it with `cgse tui` or `cgse_tui`.
|
|
7
|
+
|
|
8
|
+
The design and the status information it uses are described in `docs/dev_guide/tui_status_inventory.md`:
|
|
9
|
+
|
|
10
|
+
- the services are found in the service registry;
|
|
11
|
+
- control servers are followed live on their monitoring port, and asked for their status report when they appear or
|
|
12
|
+
are selected;
|
|
13
|
+
- the registry, logger, notification hub and metrics hub are asked for their own status.
|
|
14
|
+
|
|
15
|
+
The first version is read-only: an overview of all services and a detail screen with the status report of the
|
|
16
|
+
selected service.
|
|
17
|
+
|
|
18
|
+
Keys in the overview:
|
|
19
|
+
|
|
20
|
+
- `enter`: show the status report of the selected service;
|
|
21
|
+
- `r`: refresh, poll the registry and request the status of all services now (otherwise the status report of a
|
|
22
|
+
control server, with the device connection, is requested every 10 s);
|
|
23
|
+
- `a`: start, stop or restart the selected service (also in the detail screen), see below;
|
|
24
|
+
- `o`: select the columns of the overview (in the dialog: `space` toggles a column, `a` all, `n` none, `d` defaults,
|
|
25
|
+
`esc` or `o` closes it);
|
|
26
|
+
- `l`: show or hide the log pane; with the focus in the log pane (`tab`), `m` makes it fill the screen and back
|
|
27
|
+
(`esc` too), and `w` wraps the long lines and back. The log pane follows the new messages while it's at the end;
|
|
28
|
+
scrolled up, it stays at the same lines (`end` follows again);
|
|
29
|
+
- `L`: the log screen, also from the detail screen of the logger or with `cgse tui --logs`: the log messages of a time
|
|
30
|
+
window (the latest hour at first), read by the logger from its log files with its `history` command. `[` and `]`
|
|
31
|
+
move the window by its length, `p` and `n` by a day (yesterday), `-` and `+` make it shorter or longer (5 min to 1
|
|
32
|
+
day), `g` goes to a time, `t` to the latest window. The latest window is live: new messages are added every 2 s with
|
|
33
|
+
the `tail` command and the window moves with the time; the view follows while it's at the end (`end`). The tree at
|
|
34
|
+
the left has the loggers of the window with their number of messages; `space` selects a logger and those below it,
|
|
35
|
+
only their messages are shown. Below it the processes that logged the messages, `space` selects a process in the
|
|
36
|
+
same way (with both selected, a message must be of both); `c` clears the selection. `f`, `d` and `w` filter, hide
|
|
37
|
+
the DEBUG messages and wrap, as in the log panel of the logger;
|
|
38
|
+
- `F1`: help, on every screen (from a detail screen or the log screen it opens at the part about that screen);
|
|
39
|
+
- `ctrl+q`: quit.
|
|
40
|
+
|
|
41
|
+
The overview also shows the device drivers of the project: every node of the Setup (loaded by the configuration
|
|
42
|
+
manager) with a `cgse_object`, e.g. `gse.DAQ6510.cgse_object: daq6510`, with its `device_name` and optional
|
|
43
|
+
`device_id`. A device that runs is shown as its service in the registry; one that doesn't run is shown dimmed, with an
|
|
44
|
+
empty frame, and can be started with `a` (`cgse <object> start [<device_id>]`). See "Device drivers in the Setup" in
|
|
45
|
+
`docs/libs/cgse-common/setup.md`.
|
|
46
|
+
|
|
47
|
+
The detail screen shows the selected service in panels, updated every second:
|
|
48
|
+
|
|
49
|
+
- Service: the registration in the service registry and the endpoints (Service, Process and Device have the same
|
|
50
|
+
height);
|
|
51
|
+
- Process: PID, uptime, CPU and memory. CPU % and memory are shown in orange (warning) or red (critical) when the
|
|
52
|
+
process uses too much CPU or its memory keeps growing, with the reason below, see "CPU and memory warnings";
|
|
53
|
+
- Device: the connection and simulator mode, only for services that control a device;
|
|
54
|
+
- Details: the service specific status. For a control server these are the `details` of its status report,
|
|
55
|
+
updated with the live values of its monitoring messages (the border says `live`), for the registry, logger and
|
|
56
|
+
hubs their own status. The package of a device can show its details in its own way with a plugin (e.g. the
|
|
57
|
+
positions of the hexapods). The TUI has its own panels for the core services: the disk usage of the Storage
|
|
58
|
+
Manager, the observation and the whole Setup of the Configuration Manager, the messages per second, the queue
|
|
59
|
+
and the database of the Metrics Hub, the events of the Notification Hub, and the log file, the messages by level
|
|
60
|
+
and the last warning and error of the logger. Otherwise the details are shown as a tree, see "Details in the TUI"
|
|
61
|
+
in `docs/dev_guide/plugins.md`;
|
|
62
|
+
- Registrations: only for the service registry, all registered services; `enter` shows the details of the selected
|
|
63
|
+
service, `esc` goes back.
|
|
64
|
+
- Log: only for the logger, the last log lines, coloured by level, filling the lower part of the screen; new lines are
|
|
65
|
+
requested every 2 s with the `tail` command of the logger (a logger of an older version doesn't have it). The
|
|
66
|
+
lines follow the new messages while the view is at the end; scrolled up, the view stays at the same lines (`end`
|
|
67
|
+
follows again). `m` makes the panel fill the screen and back, `f` filters the lines on levels, process, logger or
|
|
68
|
+
module, words in the message, and a time range, and hides or dims the other lines; `d` hides the DEBUG lines, `w`
|
|
69
|
+
wraps the long lines. The panel keeps the last 1000 messages (the logger keeps 10 000 in memory), the filter applies
|
|
70
|
+
to those;
|
|
71
|
+
- Core services: only for the Process Manager, the core services it manages with their state in the TUI; `enter`
|
|
72
|
+
shows the details of the selected service, `esc` goes back.
|
|
73
|
+
|
|
74
|
+
CPU and memory warnings: the TUI judges the process of every service, and shows the CPU % and memory in orange or red,
|
|
75
|
+
also in the overview, and writes a message in the log pane when this changes:
|
|
76
|
+
|
|
77
|
+
- CPU: the average CPU % over the last minute, a warning from 50 %, critical from 90 %;
|
|
78
|
+
- memory: the growth of the memory, the RSS plus what is swapped out, over the last 10 minutes, a warning from 10 %,
|
|
79
|
+
critical from 25 %. It's judged when there are samples of at least 5 minutes, so a process that just started isn't
|
|
80
|
+
taken for a leak. The swapped-out memory counts because leaked memory ends up in swap: the RSS alone can go down
|
|
81
|
+
while a process leaks. A service of an older version doesn't report its swap, then only the RSS counts.
|
|
82
|
+
|
|
83
|
+
The limits are in `cgse_tui/data/health.py`.
|
|
84
|
+
|
|
85
|
+
Panels are dimmed when the service is stale or not running. Keys in the detail screen: `r` requests the status report
|
|
86
|
+
now (otherwise every 10 s), `a` starts, stops or restarts the service, `v` shows the raw status report, `m`, `f` and `d`
|
|
87
|
+
maximize and filter the log lines of the logger and hide its DEBUG lines, `esc` goes back to the overview.
|
|
88
|
+
|
|
89
|
+
Start, stop and restart run the `cgse <object> start|stop` command of the service, with its device identifier, e.g.
|
|
90
|
+
`cgse puna stop PUNA_01`. The dialog shows the commands; selecting an action is the confirmation. A running service
|
|
91
|
+
can be stopped or restarted, a core service that is not running can be started. With the simulator checkbox, the
|
|
92
|
+
service is started with its simulator: a simulator that is a separate process (e.g. DAQ6510, TVAC) is started first
|
|
93
|
+
with `cgse <object> start-sim` when it isn't running, other drivers are started with `--sim`. The checkbox is checked
|
|
94
|
+
when the service runs with its simulator now. With the verbose debug checkbox, the service (and its simulator) is
|
|
95
|
+
started with `VERBOSE_DEBUG=1`, otherwise without it. The progress and the result are written to the log pane: the
|
|
96
|
+
TUI waits up to 30 s until a started service is registered, or a stopped service is no longer registered. The commands
|
|
97
|
+
run in their own session, so the services started from the TUI don't belong to the terminal of the TUI.
|
|
98
|
+
|
|
99
|
+
## Latest release
|
|
100
|
+
|
|
101
|
+
- PyPI: https://pypi.org/project/cgse-tui/
|
|
102
|
+
- GitHub releases: https://github.com/IvS-KULeuven/cgse/releases
|
|
103
|
+
- Changelog: https://github.com/IvS-KULeuven/cgse/blob/main/CHANGELOG.md
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cgse-tui"
|
|
3
|
+
version = "0.26.0"
|
|
4
|
+
description = "Terminal user interface with the status of the CGSE core services and device control servers"
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "IvS KU Leuven"}
|
|
7
|
+
]
|
|
8
|
+
maintainers = [
|
|
9
|
+
{name = "Rik Huygen", email = "rik.huygen@kuleuven.be"},
|
|
10
|
+
]
|
|
11
|
+
readme = {"file" = "README.md", "content-type" = "text/markdown"}
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
license = "MIT"
|
|
14
|
+
keywords = [
|
|
15
|
+
"CGSE",
|
|
16
|
+
"Common-EGSE",
|
|
17
|
+
"hardware testing",
|
|
18
|
+
"software framework",
|
|
19
|
+
"textual",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"cgse-common",
|
|
23
|
+
"cgse-core",
|
|
24
|
+
"textual>=2.0.0",
|
|
25
|
+
"typer",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
test = ["pytest", "pytest-mock", "pytest-cov"]
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
cgse_tui = "cgse_tui.cli:main"
|
|
33
|
+
|
|
34
|
+
[project.entry-points."cgse.version"]
|
|
35
|
+
cgse-tui = 'egse.version:get_version_installed'
|
|
36
|
+
|
|
37
|
+
[project.entry-points."cgse.command"]
|
|
38
|
+
tui = 'cgse_tui.cli:tui'
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
asyncio_mode = "auto"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.sdist]
|
|
44
|
+
exclude = [
|
|
45
|
+
"/tests",
|
|
46
|
+
"/pytest.ini",
|
|
47
|
+
"/.gitignore",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.wheel]
|
|
51
|
+
packages = ["src/cgse_tui"]
|
|
52
|
+
|
|
53
|
+
[tool.ruff]
|
|
54
|
+
line-length = 120
|
|
55
|
+
|
|
56
|
+
[tool.ruff.lint]
|
|
57
|
+
select = ["F", "I"]
|
|
58
|
+
extend-select = ["E", "W"]
|
|
59
|
+
|
|
60
|
+
[tool.ruff.lint.isort]
|
|
61
|
+
force-single-line = true
|
|
62
|
+
|
|
63
|
+
[build-system]
|
|
64
|
+
requires = ["hatchling"]
|
|
65
|
+
build-backend = "hatchling.build"
|
|
66
|
+
|
|
67
|
+
[dependency-groups]
|
|
68
|
+
dev = [
|
|
69
|
+
"pytest>=8.3.4",
|
|
70
|
+
"pytest-cov>=6.0.0",
|
|
71
|
+
"pytest-mock>=3.14.0",
|
|
72
|
+
"ruff>=0.9.0",
|
|
73
|
+
"pytest-asyncio>=0.26.0",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
[project.urls]
|
|
77
|
+
Homepage = "https://github.com/IvS-KULeuven/cgse/tree/main/projects/generic/cgse-tui"
|
|
78
|
+
Repository = "https://github.com/IvS-KULeuven/cgse"
|
|
79
|
+
Releases = "https://github.com/IvS-KULeuven/cgse/releases"
|
|
80
|
+
Changelog = "https://github.com/IvS-KULeuven/cgse/blob/main/CHANGELOG.md"
|
|
81
|
+
Documentation = "https://ivs-kuleuven.github.io/cgse/"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Terminal user interface with the status of the CGSE core services and device control servers."""
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""The Textual application."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from egse.logfiles import CGSE_OBJECT as LOG_FILES_CGSE_OBJECT
|
|
9
|
+
from egse.system import get_machine_id
|
|
10
|
+
from textual.app import App
|
|
11
|
+
from textual.screen import Screen
|
|
12
|
+
|
|
13
|
+
from cgse_tui.data.actions import ServiceActions
|
|
14
|
+
from cgse_tui.data.collector import Collector
|
|
15
|
+
from cgse_tui.data.model import ServiceInfo
|
|
16
|
+
from cgse_tui.data.model import ServiceState
|
|
17
|
+
from cgse_tui.logs import BufferedLogHandler
|
|
18
|
+
from cgse_tui.logs import capture_logging
|
|
19
|
+
from cgse_tui.logs import create_log_file_handler
|
|
20
|
+
from cgse_tui.plugins import DetailsRenderers
|
|
21
|
+
from cgse_tui.screens.action_select import ActionSelectScreen
|
|
22
|
+
from cgse_tui.screens.detail import DetailScreen
|
|
23
|
+
from cgse_tui.screens.files_screen import LogFilesScreen
|
|
24
|
+
from cgse_tui.screens.help import DETAIL_SCREEN_ANCHOR
|
|
25
|
+
from cgse_tui.screens.help import LOG_SCREEN_ANCHOR
|
|
26
|
+
from cgse_tui.screens.help import HelpScreen
|
|
27
|
+
from cgse_tui.screens.log_screen import LogScreen
|
|
28
|
+
from cgse_tui.screens.overview import OverviewScreen
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger("cgse_tui.app")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CgseTuiApp(App):
|
|
34
|
+
"""Shows the status of the CGSE core services and device control servers.
|
|
35
|
+
|
|
36
|
+
While the app runs, log messages are shown in the log pane of the overview instead of on the terminal.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
collector: collects the status of the services, a default `Collector` when None.
|
|
40
|
+
refresh_interval: seconds between two updates of the overview.
|
|
41
|
+
log_file: file that gets all log messages, also those that are not shown in the log pane, e.g. the timeouts
|
|
42
|
+
of the client libraries. No log file when None.
|
|
43
|
+
actions: starts, stops and restarts the services, a default `ServiceActions` when None.
|
|
44
|
+
logs: opens the log screen at start-up, on top of the overview.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
BINDINGS = [("f1", "help", "Help")]
|
|
48
|
+
|
|
49
|
+
TITLE = "CGSE"
|
|
50
|
+
SUB_TITLE = "Status of services and devices"
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
collector: Collector | None = None,
|
|
55
|
+
refresh_interval: float = 1.0,
|
|
56
|
+
log_file: Path | None = None,
|
|
57
|
+
actions: ServiceActions | None = None,
|
|
58
|
+
logs: bool = False,
|
|
59
|
+
):
|
|
60
|
+
super().__init__()
|
|
61
|
+
self.log_file = log_file
|
|
62
|
+
self.log_handler = BufferedLogHandler()
|
|
63
|
+
self._restore_logging = None
|
|
64
|
+
self.collector = collector if collector is not None else Collector()
|
|
65
|
+
self.refresh_interval = refresh_interval
|
|
66
|
+
self.actions = actions if actions is not None else ServiceActions(self.collector)
|
|
67
|
+
self.overview = OverviewScreen()
|
|
68
|
+
self.details_renderers = DetailsRenderers()
|
|
69
|
+
self._logs_at_start = logs
|
|
70
|
+
|
|
71
|
+
def get_default_screen(self) -> Screen:
|
|
72
|
+
return self.overview
|
|
73
|
+
|
|
74
|
+
def on_load(self) -> None:
|
|
75
|
+
handlers = [self.log_handler]
|
|
76
|
+
file_error = None
|
|
77
|
+
if self.log_file is not None:
|
|
78
|
+
try:
|
|
79
|
+
handlers.append(create_log_file_handler(self.log_file))
|
|
80
|
+
except OSError as exc:
|
|
81
|
+
file_error = exc
|
|
82
|
+
self._restore_logging = capture_logging(*handlers)
|
|
83
|
+
logging.getLogger("cgse_tui").setLevel(logging.INFO) # e.g. the state changes logged by the collector
|
|
84
|
+
|
|
85
|
+
if file_error is not None:
|
|
86
|
+
logger.warning(f"Couldn't open the log file {self.log_file}: {file_error}")
|
|
87
|
+
elif self.log_file is not None:
|
|
88
|
+
logger.info(f"All log messages are also written to {self.log_file}")
|
|
89
|
+
|
|
90
|
+
def on_mount(self) -> None:
|
|
91
|
+
self.run_worker(self.collector.run(), name="collector", group="collector", exit_on_error=False)
|
|
92
|
+
self.call_after_refresh(self.update_overview) # the overview screen is mounted by then
|
|
93
|
+
self.set_interval(self.refresh_interval, self.update_overview)
|
|
94
|
+
if self._logs_at_start:
|
|
95
|
+
self.call_after_refresh(self.show_logs)
|
|
96
|
+
|
|
97
|
+
def on_unmount(self) -> None:
|
|
98
|
+
if self._restore_logging is not None:
|
|
99
|
+
self._restore_logging()
|
|
100
|
+
self._restore_logging = None
|
|
101
|
+
|
|
102
|
+
def show_details(self, service_type: str) -> None:
|
|
103
|
+
"""Opens the detail screen of a service, e.g. selected in the overview or in the registrations."""
|
|
104
|
+
service = next((s for s in self.collector.snapshot() if s.service_type == service_type), None)
|
|
105
|
+
if service is None:
|
|
106
|
+
self.notify(f"{service_type} is not known (yet).", severity="warning")
|
|
107
|
+
return
|
|
108
|
+
refresh = getattr(self.collector, "refresh_report", None)
|
|
109
|
+
self.push_screen(DetailScreen(service, refresh=refresh and (lambda: refresh(service_type))))
|
|
110
|
+
|
|
111
|
+
def find_logger(self) -> ServiceInfo | None:
|
|
112
|
+
"""The logger service, None when it isn't known (yet)."""
|
|
113
|
+
return next((s for s in self.collector.snapshot() if s.cgse_object == "log" and s.host and s.port), None)
|
|
114
|
+
|
|
115
|
+
def show_logs(self) -> None:
|
|
116
|
+
"""Opens the log screen with the latest hour of log messages, read from the log files by the logger."""
|
|
117
|
+
if not isinstance(self.screen, LogScreen):
|
|
118
|
+
self.push_screen(LogScreen(self.find_logger))
|
|
119
|
+
|
|
120
|
+
def find_log_file_services(self) -> list[ServiceInfo]:
|
|
121
|
+
"""The log-files services, one per machine and log folder, see `egse.logfiles`."""
|
|
122
|
+
return [s for s in self.collector.snapshot() if s.cgse_object == LOG_FILES_CGSE_OBJECT and s.host and s.port]
|
|
123
|
+
|
|
124
|
+
def action_show_log_files(self) -> None:
|
|
125
|
+
"""Opens the log-files screen with the files of the log folders of the machines."""
|
|
126
|
+
if not isinstance(self.screen, LogFilesScreen):
|
|
127
|
+
self.push_screen(LogFilesScreen(self.find_log_file_services))
|
|
128
|
+
|
|
129
|
+
def action_help(self) -> None:
|
|
130
|
+
"""Shows the help, at the part about the detail screen or the log screen when opened from there; F1 in the help
|
|
131
|
+
closes it."""
|
|
132
|
+
if isinstance(self.screen, HelpScreen):
|
|
133
|
+
return
|
|
134
|
+
if isinstance(self.screen, DetailScreen):
|
|
135
|
+
anchor = DETAIL_SCREEN_ANCHOR
|
|
136
|
+
elif isinstance(self.screen, LogScreen):
|
|
137
|
+
anchor = LOG_SCREEN_ANCHOR
|
|
138
|
+
else:
|
|
139
|
+
anchor = None
|
|
140
|
+
self.push_screen(HelpScreen(anchor))
|
|
141
|
+
|
|
142
|
+
def select_action(self, service: ServiceInfo) -> None:
|
|
143
|
+
"""Asks the user to start, stop or restart the service, and performs the selected action."""
|
|
144
|
+
if self.actions.is_busy(service.service_type):
|
|
145
|
+
self.notify(f"{service.name}: an action is still running.", severity="warning")
|
|
146
|
+
return
|
|
147
|
+
if service.cgse_object == LOG_FILES_CGSE_OBJECT and service.metadata.get("machine_id") != get_machine_id():
|
|
148
|
+
# `cgse lf` acts on the service of the machine where it runs, i.e. the machine of the TUI.
|
|
149
|
+
self.notify(f"{service.name} runs on another machine, use `cgse lf` there.", severity="warning")
|
|
150
|
+
return
|
|
151
|
+
is_running = getattr(self.collector, "is_running", None)
|
|
152
|
+
running = is_running(service.service_type) if is_running else service.state is not ServiceState.INACTIVE
|
|
153
|
+
|
|
154
|
+
def perform(choice: tuple[str, bool, bool] | None) -> None:
|
|
155
|
+
if choice is not None:
|
|
156
|
+
action, simulator, verbose_debug = choice
|
|
157
|
+
self.run_worker(self._perform(action, service, simulator, verbose_debug), group="actions")
|
|
158
|
+
|
|
159
|
+
simulator_support = getattr(self.actions, "simulator_support", None)
|
|
160
|
+
self.push_screen(ActionSelectScreen(service, running, simulator_support), perform)
|
|
161
|
+
|
|
162
|
+
async def _perform(self, action: str, service: ServiceInfo, simulator: bool, verbose_debug: bool = False) -> None:
|
|
163
|
+
ok = await self.actions.perform(action, service, simulator, verbose_debug=verbose_debug)
|
|
164
|
+
if not ok:
|
|
165
|
+
self.notify(f"{service.name}: {action} failed, see the log pane.", severity="error")
|
|
166
|
+
self.update_overview()
|
|
167
|
+
|
|
168
|
+
def update_overview(self) -> None:
|
|
169
|
+
services = self.collector.snapshot()
|
|
170
|
+
self.overview.show_services(services)
|
|
171
|
+
for screen in self.screen_stack:
|
|
172
|
+
if isinstance(screen, DetailScreen):
|
|
173
|
+
screen.show_services(services)
|
|
174
|
+
if messages := self.log_handler.drain():
|
|
175
|
+
self.overview.show_log_messages(messages)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""The `cgse tui` command and the `cgse_tui` script."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def tui(
|
|
9
|
+
logs: Annotated[
|
|
10
|
+
bool, typer.Option("--logs", help="Open the log screen: the log messages of the logger, read from its files.")
|
|
11
|
+
] = False,
|
|
12
|
+
):
|
|
13
|
+
"""Start the terminal user interface with the status of the CGSE services and devices.
|
|
14
|
+
|
|
15
|
+
All log messages are written to `cgse_tui.log` in the CGSE log file location.
|
|
16
|
+
"""
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from egse.env import get_log_file_location
|
|
20
|
+
|
|
21
|
+
from cgse_tui.app import CgseTuiApp # Textual is only imported when the TUI is started
|
|
22
|
+
from cgse_tui.logs import LOG_FILE_NAME
|
|
23
|
+
|
|
24
|
+
CgseTuiApp(log_file=Path(get_log_file_location()).expanduser() / LOG_FILE_NAME, logs=logs).run()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main():
|
|
28
|
+
typer.run(tui)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if __name__ == "__main__":
|
|
32
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""The data layer of the TUI. This package has no Textual imports, so it can be tested without a user interface."""
|