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 |