Stefan Schuermans commited on 2020-05-24 10:00:54
Showing 2 changed files, with 213 additions and 23 deletions.
... | ... |
@@ -2,6 +2,14 @@ |
2 | 2 |
<!-- Generated with glade 3.22.1 --> |
3 | 3 |
<interface> |
4 | 4 |
<requires lib="gtk+" version="3.20"/> |
5 |
+ <object class="GtkTreeStore" id="DetailsTree"> |
|
6 |
+ <columns> |
|
7 |
+ <!-- column-name key --> |
|
8 |
+ <column type="gchararray"/> |
|
9 |
+ <!-- column-name value --> |
|
10 |
+ <column type="gchararray"/> |
|
11 |
+ </columns> |
|
12 |
+ </object> |
|
5 | 13 |
<object class="GtkTreeStore" id="ProcessesTree"> |
6 | 14 |
<columns> |
7 | 15 |
<!-- column-name proc_id --> |
... | ... |
@@ -42,16 +50,16 @@ |
42 | 50 |
<property name="visible">True</property> |
43 | 51 |
<property name="can_focus">True</property> |
44 | 52 |
<property name="model">ProcessesTree</property> |
45 |
- <property name="reorderable">True</property> |
|
46 | 53 |
<property name="rules_hint">True</property> |
47 | 54 |
<property name="search_column">0</property> |
48 | 55 |
<property name="fixed_height_mode">True</property> |
49 | 56 |
<property name="enable_tree_lines">True</property> |
57 |
+ <signal name="cursor-changed" handler="onProcessesCursorChanged" swapped="no"/> |
|
50 | 58 |
<child internal-child="selection"> |
51 | 59 |
<object class="GtkTreeSelection"/> |
52 | 60 |
</child> |
53 | 61 |
<child> |
54 |
- <object class="GtkTreeViewColumn" id="Begin"> |
|
62 |
+ <object class="GtkTreeViewColumn" id="ProcessesBeginCol"> |
|
55 | 63 |
<property name="resizable">True</property> |
56 | 64 |
<property name="sizing">fixed</property> |
57 | 65 |
<property name="title" translatable="yes">Begin</property> |
... | ... |
@@ -60,7 +68,7 @@ |
60 | 68 |
<property name="sort_indicator">True</property> |
61 | 69 |
<property name="sort_column_id">0</property> |
62 | 70 |
<child> |
63 |
- <object class="GtkCellRendererText" id="BeginText"/> |
|
71 |
+ <object class="GtkCellRendererText" id="ProcessesBeginText"/> |
|
64 | 72 |
<attributes> |
65 | 73 |
<attribute name="text">1</attribute> |
66 | 74 |
</attributes> |
... | ... |
@@ -68,7 +76,7 @@ |
68 | 76 |
</object> |
69 | 77 |
</child> |
70 | 78 |
<child> |
71 |
- <object class="GtkTreeViewColumn" id="End"> |
|
79 |
+ <object class="GtkTreeViewColumn" id="ProcessesEndCol"> |
|
72 | 80 |
<property name="resizable">True</property> |
73 | 81 |
<property name="sizing">fixed</property> |
74 | 82 |
<property name="title" translatable="yes">End</property> |
... | ... |
@@ -77,7 +85,7 @@ |
77 | 85 |
<property name="sort_indicator">True</property> |
78 | 86 |
<property name="sort_column_id">1</property> |
79 | 87 |
<child> |
80 |
- <object class="GtkCellRendererText" id="EndText"/> |
|
88 |
+ <object class="GtkCellRendererText" id="ProcessesEndText"/> |
|
81 | 89 |
<attributes> |
82 | 90 |
<attribute name="text">2</attribute> |
83 | 91 |
</attributes> |
... | ... |
@@ -85,7 +93,7 @@ |
85 | 93 |
</object> |
86 | 94 |
</child> |
87 | 95 |
<child> |
88 |
- <object class="GtkTreeViewColumn" id="Command"> |
|
96 |
+ <object class="GtkTreeViewColumn" id="ProcessesCommandCol"> |
|
89 | 97 |
<property name="resizable">True</property> |
90 | 98 |
<property name="sizing">fixed</property> |
91 | 99 |
<property name="title" translatable="yes">Command</property> |
... | ... |
@@ -94,7 +102,7 @@ |
94 | 102 |
<property name="sort_indicator">True</property> |
95 | 103 |
<property name="sort_column_id">2</property> |
96 | 104 |
<child> |
97 |
- <object class="GtkCellRendererText" id="CommandText"/> |
|
105 |
+ <object class="GtkCellRendererText" id="ProcessesCommandText"/> |
|
98 | 106 |
<attributes> |
99 | 107 |
<attribute name="text">3</attribute> |
100 | 108 |
</attributes> |
... | ... |
@@ -111,7 +119,66 @@ |
111 | 119 |
</packing> |
112 | 120 |
</child> |
113 | 121 |
<child> |
114 |
- <placeholder/> |
|
122 |
+ <object class="GtkScrolledWindow" id="DetailsScroll"> |
|
123 |
+ <property name="width_request">256</property> |
|
124 |
+ <property name="height_request">256</property> |
|
125 |
+ <property name="visible">True</property> |
|
126 |
+ <property name="can_focus">True</property> |
|
127 |
+ <property name="shadow_type">in</property> |
|
128 |
+ <child> |
|
129 |
+ <object class="GtkTreeView" id="DetailsView"> |
|
130 |
+ <property name="visible">True</property> |
|
131 |
+ <property name="can_focus">True</property> |
|
132 |
+ <property name="model">DetailsTree</property> |
|
133 |
+ <property name="rules_hint">True</property> |
|
134 |
+ <property name="search_column">0</property> |
|
135 |
+ <property name="fixed_height_mode">True</property> |
|
136 |
+ <property name="enable_tree_lines">True</property> |
|
137 |
+ <child internal-child="selection"> |
|
138 |
+ <object class="GtkTreeSelection"/> |
|
139 |
+ </child> |
|
140 |
+ <child> |
|
141 |
+ <object class="GtkTreeViewColumn" id="DetailsKeyCol"> |
|
142 |
+ <property name="resizable">True</property> |
|
143 |
+ <property name="sizing">fixed</property> |
|
144 |
+ <property name="title" translatable="yes">Key</property> |
|
145 |
+ <property name="clickable">True</property> |
|
146 |
+ <property name="reorderable">True</property> |
|
147 |
+ <property name="sort_indicator">True</property> |
|
148 |
+ <property name="sort_column_id">0</property> |
|
149 |
+ <child> |
|
150 |
+ <object class="GtkCellRendererText" id="DetailsKeyText"/> |
|
151 |
+ <attributes> |
|
152 |
+ <attribute name="text">0</attribute> |
|
153 |
+ </attributes> |
|
154 |
+ </child> |
|
155 |
+ </object> |
|
156 |
+ </child> |
|
157 |
+ <child> |
|
158 |
+ <object class="GtkTreeViewColumn" id="DetailsValueCol"> |
|
159 |
+ <property name="resizable">True</property> |
|
160 |
+ <property name="sizing">fixed</property> |
|
161 |
+ <property name="title" translatable="yes">Value</property> |
|
162 |
+ <property name="clickable">True</property> |
|
163 |
+ <property name="reorderable">True</property> |
|
164 |
+ <property name="sort_indicator">True</property> |
|
165 |
+ <property name="sort_column_id">1</property> |
|
166 |
+ <child> |
|
167 |
+ <object class="GtkCellRendererText" id="DetailsValueText"/> |
|
168 |
+ <attributes> |
|
169 |
+ <attribute name="text">1</attribute> |
|
170 |
+ </attributes> |
|
171 |
+ </child> |
|
172 |
+ </object> |
|
173 |
+ </child> |
|
174 |
+ </object> |
|
175 |
+ </child> |
|
176 |
+ </object> |
|
177 |
+ <packing> |
|
178 |
+ <property name="expand">True</property> |
|
179 |
+ <property name="fill">True</property> |
|
180 |
+ <property name="position">1</property> |
|
181 |
+ </packing> |
|
115 | 182 |
</child> |
116 | 183 |
</object> |
117 | 184 |
<packing> |
... | ... |
@@ -1,16 +1,11 @@ |
1 | 1 |
#! /usr/bin/env python3 |
2 | 2 |
|
3 |
-import cairo |
|
4 |
-import datetime |
|
5 | 3 |
import gi |
6 | 4 |
import os |
7 | 5 |
import shlex |
8 | 6 |
import sys |
9 | 7 |
import time |
10 |
-gi.require_version('Gdk', '3.0') |
|
11 | 8 |
gi.require_version('Gtk', '3.0') |
12 |
-from gi.repository import Gdk |
|
13 |
-from gi.repository import GLib |
|
14 | 9 |
from gi.repository import Gtk |
15 | 10 |
|
16 | 11 |
import uproctrace.processes |
... | ... |
@@ -21,15 +16,77 @@ def cmdline2str(cmdline: list) -> str: |
21 | 16 |
Convert command line to string. |
22 | 17 |
""" |
23 | 18 |
if cmdline is None: |
24 |
- return "???" |
|
19 |
+ return '???' |
|
25 | 20 |
return ' '.join([shlex.quote(s) for s in cmdline]) |
26 | 21 |
|
22 |
+ |
|
23 |
+def duration2str(duration: float) -> str: |
|
24 |
+ """ |
|
25 |
+ Convert duration to string. |
|
26 |
+ """ |
|
27 |
+ if duration is None: |
|
28 |
+ return '???' |
|
29 |
+ # split into day, hours, minutes, seconds |
|
30 |
+ s = int(duration) |
|
31 |
+ m = s // 60 |
|
32 |
+ s = s % 60 |
|
33 |
+ h = m // 60 |
|
34 |
+ m = m % 60 |
|
35 |
+ d = h // 24 |
|
36 |
+ h = h % 24 |
|
37 |
+ # split into ms, us, ns |
|
38 |
+ ns = int((duration - s) * 1e9) |
|
39 |
+ us = ns // 1000 |
|
40 |
+ ns = ns % 1000 |
|
41 |
+ ms = us // 1000 |
|
42 |
+ us = us % 1000 |
|
43 |
+ # assemble text |
|
44 |
+ txt = '' |
|
45 |
+ if d > 0: |
|
46 |
+ txt += f'{d:d} d ' |
|
47 |
+ if h > 0 or txt: |
|
48 |
+ txt += f'{h:d} h ' |
|
49 |
+ if m > 0 or txt: |
|
50 |
+ txt += f'{m:d} m ' |
|
51 |
+ if s > 0 or txt: |
|
52 |
+ txt += f'{s:d} s ' |
|
53 |
+ if ms > 0 or txt: |
|
54 |
+ txt += f'{ms:d} ms ' |
|
55 |
+ if us > 0 or txt: |
|
56 |
+ txt += f'{us:d} us ' |
|
57 |
+ txt += f'{ns:d} ns ' |
|
58 |
+ txt += f'({duration:f} s)' |
|
59 |
+ return txt |
|
60 |
+ |
|
61 |
+ |
|
62 |
+def kb2str(kb: int) -> str: |
|
63 |
+ """ |
|
64 |
+ Convert size in KiB to string. |
|
65 |
+ """ |
|
66 |
+ if kb is None: |
|
67 |
+ return '???' |
|
68 |
+ # split into GiB, MiB, KiB |
|
69 |
+ mib = kb // 1024 |
|
70 |
+ kib = kb % 1024 |
|
71 |
+ gib = mib // 1024 |
|
72 |
+ mib = mib % 1024 |
|
73 |
+ # assemble text |
|
74 |
+ txt = '' |
|
75 |
+ if gib > 0: |
|
76 |
+ txt += f'{gib:d} GiB ' |
|
77 |
+ if mib > 0 or txt: |
|
78 |
+ txt += f'{mib:d} MiB ' |
|
79 |
+ txt += f'{kib:d} KiB ' |
|
80 |
+ txt += f'({kb:d} KiB)' |
|
81 |
+ return txt |
|
82 |
+ |
|
83 |
+ |
|
27 | 84 |
def timestamp2str(timestamp: float) -> str: |
28 | 85 |
""" |
29 | 86 |
Convert a timestamp to a human-reable time string." |
30 | 87 |
""" |
31 | 88 |
if timestamp is None: |
32 |
- return "???" |
|
89 |
+ return '???' |
|
33 | 90 |
sec = int(timestamp) |
34 | 91 |
nsec = int((timestamp - sec) * 1e9) |
35 | 92 |
time_str = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(sec)) |
... | ... |
@@ -37,6 +94,14 @@ def timestamp2str(timestamp: float) -> str: |
37 | 94 |
|
38 | 95 |
|
39 | 96 |
class UptGui: |
97 |
+ |
|
98 |
+ DETAIL_KEY = 0 |
|
99 |
+ DETAIL_VALUE = 1 |
|
100 |
+ PROC_PROC_ID = 0 |
|
101 |
+ PROC_BEGIN = 1 |
|
102 |
+ PROC_END = 2 |
|
103 |
+ PROC_COMMAND = 3 |
|
104 |
+ |
|
40 | 105 |
def __init__(self, proto_filename): |
41 | 106 |
""" |
42 | 107 |
Construct the GUI. |
... | ... |
@@ -44,9 +109,14 @@ class UptGui: |
44 | 109 |
self.builder = Gtk.Builder() |
45 | 110 |
script_dir = os.path.dirname(os.path.abspath(__file__)) |
46 | 111 |
self.builder.add_from_file(os.path.join(script_dir, 'upt-gui.glade')) |
112 |
+ self.widDetailsTree = self.builder.get_object('DetailsTree') |
|
113 |
+ self.widDetailsView = self.builder.get_object('DetailsView') |
|
47 | 114 |
self.widProcessesTree = self.builder.get_object('ProcessesTree') |
48 | 115 |
self.widProcessesView = self.builder.get_object('ProcessesView') |
49 |
- handlers = {'onDestroy': self.onDestroy} |
|
116 |
+ handlers = { |
|
117 |
+ 'onDestroy': self.onDestroy, |
|
118 |
+ 'onProcessesCursorChanged': self.onProcessesCursorChanged |
|
119 |
+ } |
|
50 | 120 |
self.builder.connect_signals(handlers) |
51 | 121 |
# open trace file |
52 | 122 |
self.openTrace(proto_filename) |
... | ... |
@@ -57,17 +127,65 @@ class UptGui: |
57 | 127 |
""" |
58 | 128 |
Gtk.main_quit() |
59 | 129 |
|
130 |
+ def onProcessesCursorChanged(self, widget): |
|
131 |
+ """ |
|
132 |
+ Cursor changed in processes tree view. |
|
133 |
+ """ |
|
134 |
+ # get proc_id of selected process |
|
135 |
+ proc_id = None |
|
136 |
+ proc_sel = self.widProcessesView.get_selection() |
|
137 |
+ if proc_sel is not None: |
|
138 |
+ proc_iter = proc_sel.get_selected()[1] |
|
139 |
+ if proc_iter is not None: |
|
140 |
+ proc_id = self.widProcessesTree.get_value( |
|
141 |
+ proc_iter, self.PROC_PROC_ID) |
|
142 |
+ # forget old details |
|
143 |
+ self.widDetailsTree.clear() |
|
144 |
+ # leave if no process selected |
|
145 |
+ if proc_id is None: |
|
146 |
+ return |
|
147 |
+ # get process, leave if not found |
|
148 |
+ proc = self.processes.getProcess(proc_id) |
|
149 |
+ if proc is None: |
|
150 |
+ return |
|
151 |
+ # add details of new process |
|
152 |
+ def add(key: str, value: str, parent_iter = None): |
|
153 |
+ detail_iter = self.widDetailsTree.append(parent_iter) |
|
154 |
+ self.widDetailsTree.set_value(detail_iter, self.DETAIL_KEY, key) |
|
155 |
+ self.widDetailsTree.set_value(detail_iter, self.DETAIL_VALUE, |
|
156 |
+ value) |
|
157 |
+ return detail_iter |
|
158 |
+ |
|
159 |
+ def add_list(key: str, values: list, parent_iter = None): |
|
160 |
+ if values is None: |
|
161 |
+ return add(key, '???', parent_iter) |
|
162 |
+ list_iter = add(key, f'{len(values):d} entries', parent_iter) |
|
163 |
+ for i, value in enumerate(values): |
|
164 |
+ add(f'{key} {i:d}', value, list_iter) |
|
165 |
+ |
|
166 |
+ add('begin time', timestamp2str(proc.begin_timestamp)) |
|
167 |
+ add_list('command line', proc.cmdline) |
|
168 |
+ add('CPU time', duration2str(proc.cpu_time)) |
|
169 |
+ add('end time', timestamp2str(proc.end_timestamp)) |
|
170 |
+ add_list('environment', sorted(proc.environ)) |
|
171 |
+ add('executable', proc.exe) |
|
172 |
+ add('max. resident memory', kb2str(proc.max_rss_kb)) |
|
173 |
+ add('system CPU time', duration2str(proc.sys_time)) |
|
174 |
+ add('user CPU time', duration2str(proc.user_time)) |
|
175 |
+ add('working directory', proc.cwd) |
|
176 |
+ # TODO |
|
177 |
+ |
|
60 | 178 |
def openTrace(self, proto_filename: str): |
61 | 179 |
""" |
62 | 180 |
Open a trace file. |
63 | 181 |
""" |
64 |
- # forget old data |
|
182 |
+ # forget old processes |
|
65 | 183 |
self.widProcessesTree.clear() |
66 | 184 |
# lead new data |
67 | 185 |
with open(proto_filename, 'rb') as proto_file: |
68 |
- self._processes = uproctrace.processes.Processes(proto_file) |
|
186 |
+ self.processes = uproctrace.processes.Processes(proto_file) |
|
69 | 187 |
# add processes to processes tree store |
70 |
- to_be_output = [(self._processes.toplevel, None)] |
|
188 |
+ to_be_output = [(self.processes.toplevel, None)] |
|
71 | 189 |
while to_be_output: |
72 | 190 |
procs, parent_iter = to_be_output[-1] |
73 | 191 |
if not procs: |
... | ... |
@@ -76,10 +194,15 @@ class UptGui: |
76 | 194 |
proc = procs[0] |
77 | 195 |
del procs[0] |
78 | 196 |
proc_iter = self.widProcessesTree.append(parent_iter) |
79 |
- self.widProcessesTree.set_value(proc_iter, 0, proc.proc_id) |
|
80 |
- self.widProcessesTree.set_value(proc_iter, 1, timestamp2str(proc.begin_timestamp)) |
|
81 |
- self.widProcessesTree.set_value(proc_iter, 2, timestamp2str(proc.end_timestamp)) |
|
82 |
- self.widProcessesTree.set_value(proc_iter, 3, cmdline2str(proc.cmdline)) |
|
197 |
+ self.widProcessesTree.set_value(proc_iter, self.PROC_PROC_ID, |
|
198 |
+ proc.proc_id) |
|
199 |
+ self.widProcessesTree.set_value( |
|
200 |
+ proc_iter, self.PROC_BEGIN, |
|
201 |
+ timestamp2str(proc.begin_timestamp)) |
|
202 |
+ self.widProcessesTree.set_value(proc_iter, self.PROC_END, |
|
203 |
+ timestamp2str(proc.end_timestamp)) |
|
204 |
+ self.widProcessesTree.set_value(proc_iter, self.PROC_COMMAND, |
|
205 |
+ cmdline2str(proc.cmdline)) |
|
83 | 206 |
to_be_output.append((proc.children, proc_iter)) |
84 | 207 |
# show all processes |
85 | 208 |
self.widProcessesView.expand_all() |
86 | 209 |