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.
Files changed (35) hide show
  1. cgse_tui-0.26.0/.gitignore +65 -0
  2. cgse_tui-0.26.0/PKG-INFO +127 -0
  3. cgse_tui-0.26.0/README.md +103 -0
  4. cgse_tui-0.26.0/pyproject.toml +81 -0
  5. cgse_tui-0.26.0/src/cgse_tui/__init__.py +1 -0
  6. cgse_tui-0.26.0/src/cgse_tui/app.py +175 -0
  7. cgse_tui-0.26.0/src/cgse_tui/cli.py +32 -0
  8. cgse_tui-0.26.0/src/cgse_tui/data/__init__.py +1 -0
  9. cgse_tui-0.26.0/src/cgse_tui/data/actions.py +278 -0
  10. cgse_tui-0.26.0/src/cgse_tui/data/collector.py +518 -0
  11. cgse_tui-0.26.0/src/cgse_tui/data/health.py +75 -0
  12. cgse_tui-0.26.0/src/cgse_tui/data/model.py +156 -0
  13. cgse_tui-0.26.0/src/cgse_tui/data/monitor.py +78 -0
  14. cgse_tui-0.26.0/src/cgse_tui/data/report.py +215 -0
  15. cgse_tui-0.26.0/src/cgse_tui/data/services.py +58 -0
  16. cgse_tui-0.26.0/src/cgse_tui/data/setup.py +80 -0
  17. cgse_tui-0.26.0/src/cgse_tui/details.py +377 -0
  18. cgse_tui-0.26.0/src/cgse_tui/logs.py +109 -0
  19. cgse_tui-0.26.0/src/cgse_tui/plugins.py +219 -0
  20. cgse_tui-0.26.0/src/cgse_tui/screens/__init__.py +1 -0
  21. cgse_tui-0.26.0/src/cgse_tui/screens/action_select.py +160 -0
  22. cgse_tui-0.26.0/src/cgse_tui/screens/column_select.py +95 -0
  23. cgse_tui-0.26.0/src/cgse_tui/screens/columns.py +328 -0
  24. cgse_tui-0.26.0/src/cgse_tui/screens/detail.py +481 -0
  25. cgse_tui-0.26.0/src/cgse_tui/screens/files_screen.py +330 -0
  26. cgse_tui-0.26.0/src/cgse_tui/screens/help.py +250 -0
  27. cgse_tui-0.26.0/src/cgse_tui/screens/log_filter.py +180 -0
  28. cgse_tui-0.26.0/src/cgse_tui/screens/log_screen.py +720 -0
  29. cgse_tui-0.26.0/src/cgse_tui/screens/logtail.py +188 -0
  30. cgse_tui-0.26.0/src/cgse_tui/screens/logview.py +80 -0
  31. cgse_tui-0.26.0/src/cgse_tui/screens/overview.py +202 -0
  32. cgse_tui-0.26.0/src/cgse_tui/screens/panels.py +330 -0
  33. cgse_tui-0.26.0/src/cgse_tui/screens/procman.py +71 -0
  34. cgse_tui-0.26.0/src/cgse_tui/screens/record_view.py +217 -0
  35. 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*
@@ -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."""