mrok 0.6.0__py3-none-any.whl → 0.7.0__py3-none-any.whl
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.
- mrok/agent/devtools/inspector/__main__.py +2 -24
- mrok/agent/devtools/inspector/app.py +407 -112
- mrok/agent/devtools/inspector/utils.py +149 -0
- mrok/cli/main.py +17 -1
- mrok/constants.py +21 -0
- mrok/logging.py +0 -22
- mrok/proxy/middleware.py +7 -8
- mrok/proxy/models.py +36 -10
- mrok/proxy/ziticorn.py +8 -17
- mrok/ziti/api.py +1 -1
- mrok/ziti/bootstrap.py +0 -5
- mrok/ziti/identities.py +10 -9
- {mrok-0.6.0.dist-info → mrok-0.7.0.dist-info}/METADATA +8 -2
- {mrok-0.6.0.dist-info → mrok-0.7.0.dist-info}/RECORD +17 -20
- mrok/agent/devtools/__main__.py +0 -34
- mrok/cli/commands/agent/utils.py +0 -5
- mrok/proxy/constants.py +0 -22
- mrok/proxy/utils.py +0 -90
- {mrok-0.6.0.dist-info → mrok-0.7.0.dist-info}/WHEEL +0 -0
- {mrok-0.6.0.dist-info → mrok-0.7.0.dist-info}/entry_points.txt +0 -0
- {mrok-0.6.0.dist-info → mrok-0.7.0.dist-info}/licenses/LICENSE.txt +0 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import argparse
|
|
1
2
|
import json
|
|
2
3
|
from collections import OrderedDict, deque
|
|
4
|
+
from statistics import mean
|
|
3
5
|
from typing import Literal
|
|
4
6
|
from uuid import uuid4
|
|
5
7
|
|
|
@@ -7,16 +9,19 @@ import zmq.asyncio
|
|
|
7
9
|
from rich.table import Table
|
|
8
10
|
from textual import on, work
|
|
9
11
|
from textual.app import App, ComposeResult
|
|
10
|
-
from textual.
|
|
12
|
+
from textual.binding import Binding
|
|
13
|
+
from textual.containers import Grid, Horizontal, Vertical, VerticalScroll
|
|
11
14
|
from textual.events import Resize
|
|
15
|
+
from textual.screen import ModalScreen
|
|
12
16
|
from textual.widgets import (
|
|
17
|
+
Button,
|
|
13
18
|
Collapsible,
|
|
19
|
+
ContentSwitcher,
|
|
14
20
|
DataTable,
|
|
15
21
|
Digits,
|
|
16
22
|
Header,
|
|
17
23
|
Label,
|
|
18
|
-
|
|
19
|
-
Sparkline,
|
|
24
|
+
ProgressBar,
|
|
20
25
|
Static,
|
|
21
26
|
TabbedContent,
|
|
22
27
|
TabPane,
|
|
@@ -27,30 +32,25 @@ from textual.widgets.data_table import ColumnKey
|
|
|
27
32
|
from textual.worker import get_current_worker
|
|
28
33
|
|
|
29
34
|
from mrok import __version__
|
|
30
|
-
from mrok.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
35
|
+
from mrok.agent.devtools.inspector.utils import (
|
|
36
|
+
build_tree,
|
|
37
|
+
get_highlighter_language_by_content_type,
|
|
38
|
+
hexdump,
|
|
39
|
+
humanize_bytes,
|
|
40
|
+
parse_content_type,
|
|
41
|
+
parse_form_data,
|
|
42
|
+
)
|
|
43
|
+
from mrok.proxy.models import (
|
|
44
|
+
Event,
|
|
45
|
+
HTTPHeaders,
|
|
46
|
+
HTTPRequest,
|
|
47
|
+
HTTPResponse,
|
|
48
|
+
ServiceMetadata,
|
|
49
|
+
WorkerMetrics,
|
|
50
|
+
)
|
|
45
51
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
for i in range(0, len(data), width):
|
|
49
|
-
chunk = data[i : i + width]
|
|
50
|
-
hex_part = " ".join(f"{b:02x}" for b in chunk)
|
|
51
|
-
ascii_part = "".join(chr(b) if 32 <= b <= 126 else "." for b in chunk)
|
|
52
|
-
lines.append(f"{hex_part:<{width * 3}} {ascii_part}")
|
|
53
|
-
return "\n".join(lines)
|
|
52
|
+
MIN_COLS = 160
|
|
53
|
+
MIN_ROWS = 45
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
class Counter(Digits):
|
|
@@ -84,57 +84,43 @@ class Counter(Digits):
|
|
|
84
84
|
self.border_title = self.counter_name
|
|
85
85
|
|
|
86
86
|
|
|
87
|
-
class
|
|
88
|
-
BORDER_TITLE = "Process"
|
|
87
|
+
class ProcessInfoBar(Static):
|
|
89
88
|
DEFAULT_CSS = """
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
89
|
+
ProcessInfoBar {
|
|
90
|
+
width: 100%;
|
|
91
|
+
align: center middle;
|
|
92
|
+
padding: 1;
|
|
93
|
+
}
|
|
94
|
+
ProgressBar {
|
|
95
|
+
width: 100%;
|
|
96
|
+
}
|
|
97
|
+
ProgressBar > Bar {
|
|
98
|
+
width: 24;
|
|
99
|
+
}
|
|
100
|
+
ProgressBar > Bar > .bar--complete {
|
|
101
|
+
color: red;
|
|
102
|
+
}
|
|
103
|
+
Vertical {
|
|
104
|
+
width: auto;
|
|
105
|
+
align: center middle;
|
|
99
106
|
}
|
|
100
107
|
"""
|
|
101
108
|
|
|
102
|
-
def __init__(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
*,
|
|
106
|
-
expand=False,
|
|
107
|
-
shrink=False,
|
|
108
|
-
markup=True,
|
|
109
|
-
name=None,
|
|
110
|
-
id=None,
|
|
111
|
-
classes=None,
|
|
112
|
-
disabled=False,
|
|
113
|
-
) -> None:
|
|
114
|
-
super().__init__(
|
|
115
|
-
content,
|
|
116
|
-
expand=expand,
|
|
117
|
-
shrink=shrink,
|
|
118
|
-
markup=markup,
|
|
119
|
-
name=name,
|
|
120
|
-
id=id,
|
|
121
|
-
classes=classes,
|
|
122
|
-
disabled=disabled,
|
|
123
|
-
)
|
|
124
|
-
self.mem_values: deque = deque([0] * 100, maxlen=100)
|
|
125
|
-
self.cpu_values: deque = deque([0] * 100, maxlen=100)
|
|
109
|
+
def __init__(self, label: str, **kwargs):
|
|
110
|
+
super().__init__(**kwargs)
|
|
111
|
+
self.label_text = label
|
|
126
112
|
|
|
127
113
|
def compose(self) -> ComposeResult:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
114
|
+
with Vertical():
|
|
115
|
+
yield Static(f"[b]{self.label_text}[/b]")
|
|
116
|
+
yield ProgressBar(
|
|
117
|
+
total=100,
|
|
118
|
+
show_percentage=True,
|
|
119
|
+
show_eta=False,
|
|
120
|
+
)
|
|
132
121
|
|
|
133
|
-
def
|
|
134
|
-
self.
|
|
135
|
-
self.mem_values.append(mem)
|
|
136
|
-
self.query_one("#cpu", Sparkline).data = self.cpu_values
|
|
137
|
-
self.query_one("#mem", Sparkline).data = self.mem_values
|
|
122
|
+
def update_value(self, value: float) -> None:
|
|
123
|
+
self.query_one(ProgressBar).update(progress=value)
|
|
138
124
|
|
|
139
125
|
|
|
140
126
|
class InfoPanel(Static):
|
|
@@ -178,20 +164,13 @@ class InfoPanel(Static):
|
|
|
178
164
|
table = self.query_one(DataTable)
|
|
179
165
|
table.add_columns("Label", "Value")
|
|
180
166
|
|
|
181
|
-
# if len(self.workers_metrics):
|
|
182
|
-
# hinfo = self.query_one(HostInfo)
|
|
183
|
-
# hinfo.update_info(
|
|
184
|
-
# cpu=int(mean([m.process.cpu for m in self.workers_metrics.values()])),
|
|
185
|
-
# mem=int(mean([m.process.mem for m in self.workers_metrics.values()])),
|
|
186
|
-
# )
|
|
187
|
-
|
|
188
167
|
def update_meta(self, meta: ServiceMetadata) -> None:
|
|
189
168
|
table = self.query_one(DataTable)
|
|
190
169
|
if len(table.rows) == 0:
|
|
191
170
|
table.add_row("URL", f"https://{meta.extension}.{meta.domain}")
|
|
192
171
|
table.add_row("Extension ID", meta.extension.upper())
|
|
193
172
|
table.add_row("Instance ID", meta.instance.upper())
|
|
194
|
-
table.add_row("mrok version
|
|
173
|
+
table.add_row("mrok version", __version__)
|
|
195
174
|
self.loading = False
|
|
196
175
|
|
|
197
176
|
|
|
@@ -245,9 +224,14 @@ class Details(Static):
|
|
|
245
224
|
):
|
|
246
225
|
yield Static(id="response-headers-table")
|
|
247
226
|
with TabPane("Payload"):
|
|
248
|
-
|
|
227
|
+
with ContentSwitcher(initial="payload-tree", id="payload-switcher"):
|
|
228
|
+
yield Tree("Payload", id="payload-tree")
|
|
229
|
+
yield DataTable(show_header=False, show_cursor=False, id="payload-formdata")
|
|
230
|
+
yield TextArea(id="payload-other", read_only=True)
|
|
249
231
|
with TabPane("Preview"):
|
|
250
|
-
|
|
232
|
+
with ContentSwitcher(initial="preview-tree", id="preview-switcher"):
|
|
233
|
+
yield Tree("Preview", id="preview-tree")
|
|
234
|
+
yield TextArea(id="preview-other", read_only=True)
|
|
251
235
|
with TabPane("Response"):
|
|
252
236
|
yield TextArea(id="raw-response", read_only=True)
|
|
253
237
|
|
|
@@ -267,19 +251,71 @@ class Details(Static):
|
|
|
267
251
|
self.query_one(f"#{type}-headers-table", Static).content = ""
|
|
268
252
|
|
|
269
253
|
def update_preview(self, response: HTTPResponse):
|
|
270
|
-
tree = self.query_one(Tree)
|
|
254
|
+
tree = self.query_one("#preview-tree", Tree)
|
|
271
255
|
tree.clear()
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
)
|
|
276
|
-
|
|
256
|
+
self.query_one("#preview-switcher", ContentSwitcher).current = "preview-tree"
|
|
257
|
+
if not response.body:
|
|
258
|
+
return
|
|
259
|
+
content_type_info = parse_content_type(response.headers["Content-Type"])
|
|
260
|
+
if content_type_info.content_type == "application/json":
|
|
261
|
+
build_tree(
|
|
262
|
+
tree.root,
|
|
263
|
+
json.loads(response.body.decode(encoding=content_type_info.charset)), # type: ignore[arg-type]
|
|
264
|
+
)
|
|
265
|
+
return
|
|
266
|
+
text = (
|
|
267
|
+
hexdump(response.body)
|
|
268
|
+
if content_type_info.binary
|
|
269
|
+
else response.body.decode(encoding=content_type_info.charset) # type: ignore[arg-type]
|
|
270
|
+
)
|
|
271
|
+
txtarea = self.query_one("#preview-other", TextArea)
|
|
272
|
+
txtarea.text = text
|
|
273
|
+
txtarea.language = get_highlighter_language_by_content_type(content_type_info.content_type)
|
|
274
|
+
self.query_one("#preview-switcher", ContentSwitcher).current = "preview-other"
|
|
275
|
+
|
|
276
|
+
def update_payload(self, request: HTTPRequest):
|
|
277
|
+
tree = self.query_one("#payload-tree", Tree)
|
|
278
|
+
tree.clear()
|
|
279
|
+
form_data_table = self.query_one("#payload-formdata", DataTable)
|
|
280
|
+
form_data_table.clear()
|
|
281
|
+
self.query_one("#payload-switcher", ContentSwitcher).current = "payload-tree"
|
|
282
|
+
if not request.body:
|
|
283
|
+
return
|
|
284
|
+
content_type_info = parse_content_type(request.headers["Content-Type"])
|
|
285
|
+
if content_type_info.content_type == "application/json":
|
|
286
|
+
build_tree(
|
|
287
|
+
tree.root,
|
|
288
|
+
json.loads(request.body.decode(encoding=content_type_info.charset)), # type: ignore[arg-type]
|
|
289
|
+
)
|
|
290
|
+
return
|
|
291
|
+
if content_type_info.content_type == "multipart/form-data" and not request.body_truncated:
|
|
292
|
+
for name, value in parse_form_data(request.body, content_type_info.boundary): # type: ignore[arg-type]
|
|
293
|
+
form_data_table.add_row(name, value)
|
|
294
|
+
self.query_one("#payload-switcher", ContentSwitcher).current = "payload-formdata"
|
|
295
|
+
return
|
|
296
|
+
text = (
|
|
297
|
+
hexdump(request.body)
|
|
298
|
+
if content_type_info.binary
|
|
299
|
+
else request.body.decode(encoding=content_type_info.charset) # type: ignore[arg-type]
|
|
300
|
+
)
|
|
301
|
+
txtarea = self.query_one("#payload-other", TextArea)
|
|
302
|
+
txtarea.text = text
|
|
303
|
+
self.query_one("#payload-switcher", ContentSwitcher).current = "payload-other"
|
|
277
304
|
|
|
278
305
|
def update_response(self, response: HTTPResponse):
|
|
279
306
|
text_area = self.query_one("#raw-response", TextArea)
|
|
280
307
|
text_area.clear()
|
|
281
308
|
if response.body:
|
|
282
|
-
|
|
309
|
+
content_type_info = parse_content_type(response.headers["Content-Type"])
|
|
310
|
+
text = (
|
|
311
|
+
hexdump(response.body)
|
|
312
|
+
if content_type_info.binary
|
|
313
|
+
else response.body.decode(encoding=content_type_info.charset) # type: ignore[arg-type]
|
|
314
|
+
)
|
|
315
|
+
text_area.text = text
|
|
316
|
+
text_area.language = get_highlighter_language_by_content_type(
|
|
317
|
+
content_type_info.content_type
|
|
318
|
+
)
|
|
283
319
|
|
|
284
320
|
def update_info(self, response: HTTPResponse):
|
|
285
321
|
general_table = self.query_one("#general-data", DataTable)
|
|
@@ -292,6 +328,7 @@ class Details(Static):
|
|
|
292
328
|
general_table.add_row("Status Code", self.format_status(response.status))
|
|
293
329
|
self.update_headers("request", response.request.headers)
|
|
294
330
|
self.update_headers("response", response.headers)
|
|
331
|
+
self.update_payload(response.request)
|
|
295
332
|
self.update_preview(response)
|
|
296
333
|
self.update_response(response)
|
|
297
334
|
|
|
@@ -315,6 +352,8 @@ class Details(Static):
|
|
|
315
352
|
def on_mount(self) -> None:
|
|
316
353
|
general_table = self.query_one("#general-data", DataTable)
|
|
317
354
|
general_table.add_columns("Label", "Value")
|
|
355
|
+
form_data_table = self.query_one("#payload-formdata", DataTable)
|
|
356
|
+
form_data_table.add_columns("Name", "Value")
|
|
318
357
|
|
|
319
358
|
|
|
320
359
|
class LeftPanel(Static):
|
|
@@ -322,11 +361,11 @@ class LeftPanel(Static):
|
|
|
322
361
|
LeftPanel {
|
|
323
362
|
layout: grid;
|
|
324
363
|
grid-size: 1 3;
|
|
325
|
-
grid-rows:
|
|
364
|
+
grid-rows: 6 2fr 4fr;
|
|
326
365
|
grid-columns: 1fr;
|
|
327
366
|
background: black;
|
|
328
367
|
height: 100%;
|
|
329
|
-
width:
|
|
368
|
+
width: 5fr;
|
|
330
369
|
}
|
|
331
370
|
Details {
|
|
332
371
|
background: black;
|
|
@@ -341,15 +380,74 @@ class LeftPanel(Static):
|
|
|
341
380
|
yield Details()
|
|
342
381
|
|
|
343
382
|
|
|
344
|
-
class
|
|
345
|
-
BORDER_TITLE = "
|
|
383
|
+
class ProcessMetrics(Static):
|
|
384
|
+
BORDER_TITLE = "Process"
|
|
385
|
+
DEFAULT_CSS = """
|
|
386
|
+
ProcessMetrics {
|
|
387
|
+
border: round #00BBFF;
|
|
388
|
+
layout: grid;
|
|
389
|
+
grid-size: 1 2;
|
|
390
|
+
grid-gutter: 0;
|
|
391
|
+
height: 100%;
|
|
392
|
+
align: center middle;
|
|
393
|
+
}
|
|
394
|
+
"""
|
|
395
|
+
|
|
396
|
+
def __init__(
|
|
397
|
+
self,
|
|
398
|
+
content="",
|
|
399
|
+
*,
|
|
400
|
+
expand=False,
|
|
401
|
+
shrink=False,
|
|
402
|
+
markup=True,
|
|
403
|
+
name=None,
|
|
404
|
+
id=None,
|
|
405
|
+
classes=None,
|
|
406
|
+
disabled=False,
|
|
407
|
+
) -> None:
|
|
408
|
+
super().__init__(
|
|
409
|
+
content,
|
|
410
|
+
expand=expand,
|
|
411
|
+
shrink=shrink,
|
|
412
|
+
markup=markup,
|
|
413
|
+
name=name,
|
|
414
|
+
id=id,
|
|
415
|
+
classes=classes,
|
|
416
|
+
disabled=disabled,
|
|
417
|
+
)
|
|
418
|
+
self.mem_values: deque = deque([0] * 10, maxlen=10)
|
|
419
|
+
self.cpu_values: deque = deque([0] * 10, maxlen=10)
|
|
420
|
+
self.workers_metrics: dict[str, WorkerMetrics] = {}
|
|
421
|
+
|
|
422
|
+
def compose(self) -> ComposeResult:
|
|
423
|
+
yield ProcessInfoBar("CPU", id="cpu")
|
|
424
|
+
yield ProcessInfoBar("Memory", id="mem")
|
|
425
|
+
|
|
426
|
+
def on_mount(self) -> None:
|
|
427
|
+
self.query_one("#cpu", ProcessInfoBar).update_value(0)
|
|
428
|
+
self.query_one("#mem", ProcessInfoBar).update_value(0)
|
|
429
|
+
|
|
430
|
+
def update_stats(self) -> None:
|
|
431
|
+
cpu = mean([m.process.cpu for m in self.workers_metrics.values()])
|
|
432
|
+
mem = mean([m.process.mem for m in self.workers_metrics.values()])
|
|
433
|
+
|
|
434
|
+
self.query_one("#cpu", ProcessInfoBar).update_value(cpu)
|
|
435
|
+
self.query_one("#mem", ProcessInfoBar).update_value(mem)
|
|
436
|
+
|
|
437
|
+
def update_metrics(self, metrics: WorkerMetrics) -> None:
|
|
438
|
+
self.workers_metrics[metrics.worker_id] = metrics
|
|
439
|
+
self.update_stats()
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
class RequestsMetrics(Static):
|
|
443
|
+
BORDER_TITLE = "Requests"
|
|
346
444
|
DEFAULT_CSS = """
|
|
347
|
-
|
|
445
|
+
RequestsMetrics {
|
|
348
446
|
background: black;
|
|
349
447
|
layout: grid;
|
|
350
448
|
grid-size: 1 4;
|
|
351
449
|
grid-gutter: 0;
|
|
352
|
-
|
|
450
|
+
align-vertical: middle;
|
|
353
451
|
border: round #0088FF;
|
|
354
452
|
height: 100%;
|
|
355
453
|
grid-rows: auto auto auto auto;
|
|
@@ -396,7 +494,7 @@ class MetricsPanel(Static):
|
|
|
396
494
|
yield Counter("Failed", id="requests-ko")
|
|
397
495
|
|
|
398
496
|
def on_mount(self) -> None:
|
|
399
|
-
self.
|
|
497
|
+
self.update_stats()
|
|
400
498
|
|
|
401
499
|
def update_stats(self) -> None:
|
|
402
500
|
self.query_one("#requests-rps", Digits).update(
|
|
@@ -413,26 +511,175 @@ class MetricsPanel(Static):
|
|
|
413
511
|
)
|
|
414
512
|
self.loading = False
|
|
415
513
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
514
|
+
def update_metrics(self, metrics: WorkerMetrics) -> None:
|
|
515
|
+
self.workers_metrics[metrics.worker_id] = metrics
|
|
516
|
+
self.update_stats()
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
class DataTransferMetrics(Static):
|
|
520
|
+
BORDER_TITLE = "Transfer"
|
|
521
|
+
DEFAULT_CSS = """
|
|
522
|
+
DataTransferMetrics {
|
|
523
|
+
background: black;
|
|
524
|
+
layout: grid;
|
|
525
|
+
grid-size: 1 2;
|
|
526
|
+
grid-gutter: 0;
|
|
527
|
+
align-vertical: middle;
|
|
528
|
+
border: round #0088FF;
|
|
529
|
+
height: 100%;
|
|
530
|
+
grid-rows: auto auto auto auto;
|
|
531
|
+
}
|
|
532
|
+
"""
|
|
533
|
+
|
|
534
|
+
def __init__(
|
|
535
|
+
self,
|
|
536
|
+
content="",
|
|
537
|
+
*,
|
|
538
|
+
expand=False,
|
|
539
|
+
shrink=False,
|
|
540
|
+
markup=True,
|
|
541
|
+
name=None,
|
|
542
|
+
id=None,
|
|
543
|
+
classes=None,
|
|
544
|
+
disabled=False,
|
|
545
|
+
) -> None:
|
|
546
|
+
super().__init__(
|
|
547
|
+
content,
|
|
548
|
+
expand=expand,
|
|
549
|
+
shrink=shrink,
|
|
550
|
+
markup=markup,
|
|
551
|
+
name=name,
|
|
552
|
+
id=id,
|
|
553
|
+
classes=classes,
|
|
554
|
+
disabled=disabled,
|
|
555
|
+
)
|
|
556
|
+
self.workers_metrics: dict[str, WorkerMetrics] = {}
|
|
557
|
+
|
|
558
|
+
def compose(self) -> ComposeResult:
|
|
559
|
+
yield Counter("In", id="in")
|
|
560
|
+
yield Counter("Out", id="out")
|
|
561
|
+
|
|
562
|
+
def on_mount(self) -> None:
|
|
563
|
+
self.update_stats()
|
|
564
|
+
|
|
565
|
+
def update_stats(self) -> None:
|
|
566
|
+
amount, unit = humanize_bytes(
|
|
567
|
+
sum(m.data_transfer.bytes_in for m in self.workers_metrics.values())
|
|
568
|
+
)
|
|
569
|
+
self.query_one("#in", Digits).border_title = f"In ({unit})"
|
|
570
|
+
self.query_one("#in", Digits).update(str(amount))
|
|
571
|
+
amount, unit = humanize_bytes(
|
|
572
|
+
sum(m.data_transfer.bytes_out for m in self.workers_metrics.values())
|
|
573
|
+
)
|
|
574
|
+
self.query_one("#out", Digits).border_title = f"Out ({unit})"
|
|
575
|
+
self.query_one("#out", Digits).update(str(amount))
|
|
576
|
+
self.loading = False
|
|
422
577
|
|
|
423
578
|
def update_metrics(self, metrics: WorkerMetrics) -> None:
|
|
424
579
|
self.workers_metrics[metrics.worker_id] = metrics
|
|
580
|
+
self.update_stats()
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
class RightPanel(Static):
|
|
584
|
+
DEFAULT_CSS = """
|
|
585
|
+
RightPanel {
|
|
586
|
+
layout: grid;
|
|
587
|
+
grid-size: 1 3;
|
|
588
|
+
grid-rows: 1fr 22 12;
|
|
589
|
+
grid-columns: 1fr;
|
|
590
|
+
background: black;
|
|
591
|
+
height: 100%;
|
|
592
|
+
width: 33;
|
|
593
|
+
}
|
|
594
|
+
.hidden {
|
|
595
|
+
display: none;
|
|
596
|
+
}
|
|
597
|
+
"""
|
|
598
|
+
|
|
599
|
+
def compose(self):
|
|
600
|
+
yield ProcessMetrics()
|
|
601
|
+
yield RequestsMetrics()
|
|
602
|
+
yield DataTransferMetrics()
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
class TooSmallScreen(ModalScreen):
|
|
606
|
+
CSS = """
|
|
607
|
+
TooSmallScreen {
|
|
608
|
+
align: center middle;
|
|
609
|
+
}
|
|
610
|
+
#dialog {
|
|
611
|
+
grid-size: 1 3;
|
|
612
|
+
grid-rows: 1fr 2fr 3;
|
|
613
|
+
padding: 0 1;
|
|
614
|
+
width: 60;
|
|
615
|
+
height: 15;
|
|
616
|
+
border: thick $background 80%;
|
|
617
|
+
background: $surface;
|
|
618
|
+
}
|
|
619
|
+
#title {
|
|
620
|
+
column-span: 1;
|
|
621
|
+
height: 1fr;
|
|
622
|
+
width: 1fr;
|
|
623
|
+
content-align: center middle;
|
|
624
|
+
background: $panel;
|
|
625
|
+
color: $foreground;
|
|
626
|
+
text-style: bold;
|
|
627
|
+
}
|
|
628
|
+
#message {
|
|
629
|
+
margin-top: 1;
|
|
630
|
+
column-span: 1;
|
|
631
|
+
height: 1fr;
|
|
632
|
+
width: 1fr;
|
|
633
|
+
content-align: center top;
|
|
634
|
+
}
|
|
635
|
+
Button {
|
|
636
|
+
width: 100%;
|
|
637
|
+
}
|
|
638
|
+
"""
|
|
639
|
+
|
|
640
|
+
def __init__(self):
|
|
641
|
+
super().__init__()
|
|
642
|
+
self.dialog_title = "Terminal too small"
|
|
643
|
+
self.dialog_message = (
|
|
644
|
+
f"Your current terminal size is {self.app.size.width}x{self.app.size.height}. "
|
|
645
|
+
f"For the best experience please resize your terminal to {MIN_COLS}x{MIN_ROWS}."
|
|
646
|
+
)
|
|
647
|
+
self.btn_label = "dismiss"
|
|
648
|
+
self.btn_variant = "primary"
|
|
649
|
+
|
|
650
|
+
def compose(self) -> ComposeResult:
|
|
651
|
+
yield Grid(
|
|
652
|
+
Label(self.dialog_title, id="title"),
|
|
653
|
+
Label(self.dialog_message, id="message"),
|
|
654
|
+
Button(self.btn_label, variant=self.btn_variant, id="dismiss"),
|
|
655
|
+
id="dialog",
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
@on(Button.Pressed)
|
|
659
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
660
|
+
event.stop()
|
|
661
|
+
if event.button.id == "dismiss":
|
|
662
|
+
self.app.pop_screen()
|
|
663
|
+
|
|
664
|
+
def update_message(self):
|
|
665
|
+
self.query_one("#message", Label).content = (
|
|
666
|
+
f"Your current terminal size is {self.app.size.width}x{self.app.size.height}. "
|
|
667
|
+
f"For the best experience please resize your terminal to {MIN_COLS}x{MIN_ROWS}."
|
|
668
|
+
)
|
|
425
669
|
|
|
426
670
|
|
|
427
671
|
class InspectorApp(App):
|
|
428
672
|
TITLE = "mrok Dev Console"
|
|
429
673
|
CSS = """
|
|
430
674
|
Screen {
|
|
431
|
-
layout: grid;
|
|
432
|
-
grid-size: 2 1;
|
|
433
|
-
grid-columns: 5fr 1fr;
|
|
434
675
|
background: black;
|
|
435
676
|
}
|
|
677
|
+
# Screen {
|
|
678
|
+
# layout: grid;
|
|
679
|
+
# grid-size: 2 1;
|
|
680
|
+
# grid-columns: 5fr 1fr;
|
|
681
|
+
# background: black;
|
|
682
|
+
# }
|
|
436
683
|
DataTable {
|
|
437
684
|
background: black;
|
|
438
685
|
color: #dddddd;
|
|
@@ -480,8 +727,21 @@ class InspectorApp(App):
|
|
|
480
727
|
background: black;
|
|
481
728
|
border: round #00BBFF;
|
|
482
729
|
}
|
|
730
|
+
Tree {
|
|
731
|
+
background: black;
|
|
732
|
+
}
|
|
733
|
+
TextArea {
|
|
734
|
+
background: black;
|
|
735
|
+
}
|
|
736
|
+
RightPanel.-hidden {
|
|
737
|
+
display: none;
|
|
738
|
+
}
|
|
483
739
|
"""
|
|
484
740
|
|
|
741
|
+
BINDINGS = [
|
|
742
|
+
Binding("m", "toggle_metrics()", "Toggle Metrics"),
|
|
743
|
+
]
|
|
744
|
+
|
|
485
745
|
def __init__(
|
|
486
746
|
self,
|
|
487
747
|
subscriber_port: int,
|
|
@@ -500,29 +760,35 @@ class InspectorApp(App):
|
|
|
500
760
|
|
|
501
761
|
def compose(self) -> ComposeResult:
|
|
502
762
|
yield Header()
|
|
503
|
-
|
|
504
|
-
|
|
763
|
+
with Horizontal():
|
|
764
|
+
yield LeftPanel()
|
|
765
|
+
yield RightPanel()
|
|
505
766
|
|
|
506
767
|
async def on_mount(self):
|
|
507
|
-
self.
|
|
508
|
-
self.query_one(MetricsPanel).loading = True
|
|
768
|
+
self._check_minimum_size(self.size.width, self.size.height)
|
|
509
769
|
self.socket.subscribe("")
|
|
510
770
|
self.consumer()
|
|
511
771
|
|
|
772
|
+
async def on_unmount(self):
|
|
773
|
+
self.socket.close()
|
|
774
|
+
self.ctx.term()
|
|
775
|
+
|
|
512
776
|
@work(exclusive=True)
|
|
513
777
|
async def consumer(self):
|
|
514
778
|
worker = get_current_worker()
|
|
515
779
|
requests_table = self.query_one("#requests", DataTable)
|
|
516
780
|
while not worker.is_cancelled:
|
|
517
781
|
try:
|
|
518
|
-
self.log("Waiting for event")
|
|
519
782
|
event = Event.model_validate_json(await self.socket.recv_string())
|
|
520
|
-
self.log(f"Event received: {event}")
|
|
521
783
|
if event.type == "status":
|
|
522
784
|
info_widget = self.query_one(InfoPanel)
|
|
523
785
|
info_widget.update_meta(event.data.meta)
|
|
524
|
-
|
|
525
|
-
|
|
786
|
+
process_metrics_widget = self.query_one(ProcessMetrics)
|
|
787
|
+
process_metrics_widget.update_metrics(event.data.metrics)
|
|
788
|
+
requests_metrics_widget = self.query_one(RequestsMetrics)
|
|
789
|
+
requests_metrics_widget.update_metrics(event.data.metrics)
|
|
790
|
+
data_transfer_metrics_widget = self.query_one(DataTransferMetrics)
|
|
791
|
+
data_transfer_metrics_widget.update_metrics(event.data.metrics)
|
|
526
792
|
continue
|
|
527
793
|
|
|
528
794
|
response = event.data
|
|
@@ -542,6 +808,9 @@ class InspectorApp(App):
|
|
|
542
808
|
except Exception as e:
|
|
543
809
|
self.log(e)
|
|
544
810
|
|
|
811
|
+
def action_toggle_metrics(self):
|
|
812
|
+
self.query_one(RightPanel).toggle_class("-hidden")
|
|
813
|
+
|
|
545
814
|
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
|
546
815
|
if not event.row_key:
|
|
547
816
|
return
|
|
@@ -550,7 +819,33 @@ class InspectorApp(App):
|
|
|
550
819
|
details.update_info(response)
|
|
551
820
|
|
|
552
821
|
@on(Resize)
|
|
553
|
-
def
|
|
822
|
+
def handle_resize(self, event: Resize):
|
|
823
|
+
self._check_minimum_size(event.size.width, event.size.height)
|
|
554
824
|
width = event.size.width
|
|
555
825
|
table = self.query_one("#requests", DataTable)
|
|
556
826
|
table.columns[ColumnKey("path")].width = width - 69
|
|
827
|
+
|
|
828
|
+
def _check_minimum_size(self, width: int, height: int):
|
|
829
|
+
if width < MIN_COLS or height < MIN_ROWS:
|
|
830
|
+
if not isinstance(self.screen, TooSmallScreen):
|
|
831
|
+
self.push_screen(TooSmallScreen())
|
|
832
|
+
else:
|
|
833
|
+
self.screen.update_message()
|
|
834
|
+
return
|
|
835
|
+
|
|
836
|
+
if isinstance(self.screen, TooSmallScreen):
|
|
837
|
+
self.pop_screen()
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def module_main(argv: list[str]) -> None:
|
|
841
|
+
parser = argparse.ArgumentParser(description="mrok devtools agent")
|
|
842
|
+
parser.add_argument(
|
|
843
|
+
"-p",
|
|
844
|
+
"--subscriber-port",
|
|
845
|
+
type=int,
|
|
846
|
+
default=50001,
|
|
847
|
+
help="Port for subscriber (default: 50001)",
|
|
848
|
+
)
|
|
849
|
+
args = parser.parse_args(argv)
|
|
850
|
+
app = InspectorApp(args.subscriber_port)
|
|
851
|
+
app.run()
|