gini-toolkit 6.0.1.dev0__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.
- gini/__init__.py +12 -0
- gini/__main__.py +107 -0
- gini/_version.py +24 -0
- gini/agent/__init__.py +17 -0
- gini/agent/agent_gamemaster.py +140 -0
- gini/agent/api.py +291 -0
- gini/agent/ask.py +123 -0
- gini/agent/authoring.py +72 -0
- gini/agent/blackboard.py +114 -0
- gini/agent/contracts.py +142 -0
- gini/agent/domains.py +91 -0
- gini/agent/embed.py +123 -0
- gini/agent/gamemaster.py +256 -0
- gini/agent/kb.py +148 -0
- gini/agent/lesson_resolver.py +261 -0
- gini/agent/llm/__init__.py +5 -0
- gini/agent/llm/backend.py +43 -0
- gini/agent/llm/fake.py +25 -0
- gini/agent/llm/ollama.py +206 -0
- gini/agent/loop.py +258 -0
- gini/agent/mcp_server.py +86 -0
- gini/agent/meaning.py +225 -0
- gini/agent/mission.py +210 -0
- gini/agent/mission_controller.py +208 -0
- gini/agent/narration.py +116 -0
- gini/agent/notifier.py +86 -0
- gini/agent/personas.py +79 -0
- gini/agent/reasoning.py +172 -0
- gini/agent/recall.py +248 -0
- gini/agent/session.py +79 -0
- gini/agent/teaching_center.py +482 -0
- gini/agent/tools/__init__.py +3 -0
- gini/agent/tools/registry.py +193 -0
- gini/agent/twin/__init__.py +28 -0
- gini/agent/twin/authoring.py +71 -0
- gini/agent/twin/contracts.py +54 -0
- gini/agent/twin/dialectic.py +189 -0
- gini/agent/twin/harness.py +93 -0
- gini/agent/twin/justify.py +156 -0
- gini/agent/twin/learner.py +64 -0
- gini/agent/twin/mission.py +60 -0
- gini/agent/twin/os_coach.py +79 -0
- gini/agent/twin/salience.py +30 -0
- gini/agent/understand.py +250 -0
- gini/agent/verifiers.py +106 -0
- gini/agent/wizard.py +178 -0
- gini/agent/xv6_pack.py +74 -0
- gini/app/__init__.py +3 -0
- gini/app/context.py +368 -0
- gini/app/paths.py +121 -0
- gini/data/README.md +21 -0
- gini/domain/__init__.py +9 -0
- gini/domain/assembly.py +209 -0
- gini/domain/authoring.py +353 -0
- gini/domain/blueprints.py +5 -0
- gini/domain/capabilities.py +177 -0
- gini/domain/catalog.py +85 -0
- gini/domain/certify.py +201 -0
- gini/domain/compose.py +413 -0
- gini/domain/composition.py +88 -0
- gini/domain/concepts.py +383 -0
- gini/domain/connection_rules.py +269 -0
- gini/domain/constraints.py +153 -0
- gini/domain/content.py +59 -0
- gini/domain/cpu_journey.py +89 -0
- gini/domain/devices.py +747 -0
- gini/domain/diagnose.py +201 -0
- gini/domain/element_guide.py +327 -0
- gini/domain/explain.py +90 -0
- gini/domain/fingerprint.py +201 -0
- gini/domain/firewall.py +34 -0
- gini/domain/flowlog.py +61 -0
- gini/domain/flowtable.py +179 -0
- gini/domain/fragment_yaml.py +230 -0
- gini/domain/fragments.py +169 -0
- gini/domain/games/__init__.py +2 -0
- gini/domain/games/paging_games.py +119 -0
- gini/domain/games/policy_game.py +86 -0
- gini/domain/games/process_game.py +48 -0
- gini/domain/games/thrash_game.py +75 -0
- gini/domain/games/translate_game.py +60 -0
- gini/domain/games/trap_game.py +86 -0
- gini/domain/grader.py +155 -0
- gini/domain/grouping.py +67 -0
- gini/domain/legality.py +103 -0
- gini/domain/lesson.py +241 -0
- gini/domain/lexicon.py +150 -0
- gini/domain/machine_state.py +410 -0
- gini/domain/missions/networking/basic-lan.yaml +32 -0
- gini/domain/missions/networking/cache-in-front.yaml +23 -0
- gini/domain/missions/networking/decouple-with-queue.yaml +31 -0
- gini/domain/missions/networking/drive-load.yaml +20 -0
- gini/domain/missions/networking/fix-the-address.yaml +75 -0
- gini/domain/missions/networking/fix-the-lan.yaml +43 -0
- gini/domain/missions/networking/inspect-flows.yaml +16 -0
- gini/domain/missions/networking/k8s-autoscale.yaml +27 -0
- gini/domain/missions/networking/least-privilege.yaml +21 -0
- gini/domain/missions/networking/load-balanced-web.yaml +29 -0
- gini/domain/missions/networking/observe-it.yaml +24 -0
- gini/domain/missions/networking/put-in-vpc.yaml +30 -0
- gini/domain/missions/networking/reachability-boundary.yaml +56 -0
- gini/domain/missions/networking/sdn-reactive.yaml +35 -0
- gini/domain/missions/networking/send-request.yaml +19 -0
- gini/domain/missions/networking/serverless-api.yaml +25 -0
- gini/domain/missions/networking/service-chain.yaml +33 -0
- gini/domain/missions/os/lottery-fix.yaml +19 -0
- gini/domain/missions/os/priority-fix.yaml +24 -0
- gini/domain/missions.py +111 -0
- gini/domain/modulechain.py +36 -0
- gini/domain/objectives.py +488 -0
- gini/domain/os_zoo.py +79 -0
- gini/domain/paging_sim.py +141 -0
- gini/domain/pricing.py +199 -0
- gini/domain/probes.py +226 -0
- gini/domain/profile.py +142 -0
- gini/domain/recipes.py +738 -0
- gini/domain/riders.py +309 -0
- gini/domain/router_modules.py +224 -0
- gini/domain/routetable.py +67 -0
- gini/domain/scoring.py +76 -0
- gini/domain/staging.py +122 -0
- gini/domain/syscall_builder.py +144 -0
- gini/domain/topic_cloud.py +62 -0
- gini/domain/topology.py +213 -0
- gini/domain/vocabulary.py +51 -0
- gini/domain/xv6.py +808 -0
- gini/domain/xv6_fs.py +250 -0
- gini/domain/xv6_runner.py +113 -0
- gini/domain/xv6_vm.py +385 -0
- gini/gloader.py +17 -0
- gini/runtime/__init__.py +18 -0
- gini/runtime/cloudfabric_agent.py +370 -0
- gini/runtime/console.py +68 -0
- gini/runtime/control.py +70 -0
- gini/runtime/frame.py +138 -0
- gini/runtime/gbridge.py +638 -0
- gini/runtime/grouter.py +223 -0
- gini/runtime/hostsim.py +90 -0
- gini/runtime/shuttle.py +348 -0
- gini/runtime/switch.py +109 -0
- gini/runtime/transport.py +77 -0
- gini/runtime/xv6_bridge.py +312 -0
- gini/server/__init__.py +22 -0
- gini/server/__main__.py +74 -0
- gini/server/app.py +140 -0
- gini/server/auth.py +82 -0
- gini/server/policy.py +57 -0
- gini/server/session.py +23 -0
- gini/services/__init__.py +15 -0
- gini/services/boardflash.py +248 -0
- gini/services/boardsetup.py +374 -0
- gini/services/cloud_catalog.py +143 -0
- gini/services/compiler.py +1858 -0
- gini/services/discovery.py +324 -0
- gini/services/gloader.py +183 -0
- gini/services/orchestrator.py +1460 -0
- gini/services/persistence.py +28 -0
- gini/services/probe_runner.py +149 -0
- gini/services/project.py +217 -0
- gini/services/remote.py +93 -0
- gini/services/rider_runner.py +96 -0
- gini/services/rider_session.py +171 -0
- gini/services/shadow_store.py +52 -0
- gini/services/terminal.py +45 -0
- gini/setup/__init__.py +17 -0
- gini/setup/cli.py +109 -0
- gini/setup/images.py +33 -0
- gini/setup/marker.py +43 -0
- gini/setup/runtime.py +69 -0
- gini/ui/__init__.py +3 -0
- gini/ui/assets/app_icon.icns +0 -0
- gini/ui/assets/app_icon.ico +0 -0
- gini/ui/assets/app_icon.png +0 -0
- gini/ui/assets/app_icon_1024.png +0 -0
- gini/ui/assets/cue/_w.txt +1 -0
- gini/ui/assets/cue/ai.png +0 -0
- gini/ui/assets/cue/canvas.png +0 -0
- gini/ui/assets/cue/cloud.png +0 -0
- gini/ui/assets/cue/cost.png +0 -0
- gini/ui/assets/cue/dark/ai.png +0 -0
- gini/ui/assets/cue/dark/canvas.png +0 -0
- gini/ui/assets/cue/dark/cloud.png +0 -0
- gini/ui/assets/cue/dark/cost.png +0 -0
- gini/ui/assets/cue/dark/metrics.png +0 -0
- gini/ui/assets/cue/dark/router.png +0 -0
- gini/ui/assets/cue/dark/run.png +0 -0
- gini/ui/assets/cue/dark/serverless.png +0 -0
- gini/ui/assets/cue/dark/settings.png +0 -0
- gini/ui/assets/cue/dark/welcome.png +0 -0
- gini/ui/assets/cue/dark/wizard.png +0 -0
- gini/ui/assets/cue/ginibrand/ai.png +0 -0
- gini/ui/assets/cue/ginibrand/canvas.png +0 -0
- gini/ui/assets/cue/ginibrand/cloud.png +0 -0
- gini/ui/assets/cue/ginibrand/cost.png +0 -0
- gini/ui/assets/cue/ginibrand/metrics.png +0 -0
- gini/ui/assets/cue/ginibrand/router.png +0 -0
- gini/ui/assets/cue/ginibrand/run.png +0 -0
- gini/ui/assets/cue/ginibrand/serverless.png +0 -0
- gini/ui/assets/cue/ginibrand/settings.png +0 -0
- gini/ui/assets/cue/ginibrand/welcome.png +0 -0
- gini/ui/assets/cue/ginibrand/wizard.png +0 -0
- gini/ui/assets/cue/highcontrast/ai.png +0 -0
- gini/ui/assets/cue/highcontrast/canvas.png +0 -0
- gini/ui/assets/cue/highcontrast/cloud.png +0 -0
- gini/ui/assets/cue/highcontrast/cost.png +0 -0
- gini/ui/assets/cue/highcontrast/metrics.png +0 -0
- gini/ui/assets/cue/highcontrast/router.png +0 -0
- gini/ui/assets/cue/highcontrast/run.png +0 -0
- gini/ui/assets/cue/highcontrast/serverless.png +0 -0
- gini/ui/assets/cue/highcontrast/settings.png +0 -0
- gini/ui/assets/cue/highcontrast/welcome.png +0 -0
- gini/ui/assets/cue/highcontrast/wizard.png +0 -0
- gini/ui/assets/cue/light/ai.png +0 -0
- gini/ui/assets/cue/light/canvas.png +0 -0
- gini/ui/assets/cue/light/cloud.png +0 -0
- gini/ui/assets/cue/light/cost.png +0 -0
- gini/ui/assets/cue/light/metrics.png +0 -0
- gini/ui/assets/cue/light/router.png +0 -0
- gini/ui/assets/cue/light/run.png +0 -0
- gini/ui/assets/cue/light/serverless.png +0 -0
- gini/ui/assets/cue/light/settings.png +0 -0
- gini/ui/assets/cue/light/welcome.png +0 -0
- gini/ui/assets/cue/light/wizard.png +0 -0
- gini/ui/assets/cue/metrics.png +0 -0
- gini/ui/assets/cue/router.png +0 -0
- gini/ui/assets/cue/run.png +0 -0
- gini/ui/assets/cue/serverless.png +0 -0
- gini/ui/assets/cue/settings.png +0 -0
- gini/ui/assets/cue/welcome.png +0 -0
- gini/ui/assets/cue/wizard.png +0 -0
- gini/ui/assistant.py +2111 -0
- gini/ui/author_dialog.py +184 -0
- gini/ui/board_dialog.py +247 -0
- gini/ui/branding.py +21 -0
- gini/ui/canvas.py +2007 -0
- gini/ui/chat_panel.py +7 -0
- gini/ui/cpu_journey.py +212 -0
- gini/ui/cpu_lab.py +306 -0
- gini/ui/cue_cards.py +214 -0
- gini/ui/dashboard.py +222 -0
- gini/ui/diagnose_game.py +336 -0
- gini/ui/fingerprint_lab.py +219 -0
- gini/ui/flash_dialog.py +244 -0
- gini/ui/flow_layout.py +63 -0
- gini/ui/fragment_manager.py +1415 -0
- gini/ui/game_catalog.py +184 -0
- gini/ui/game_renderers.py +340 -0
- gini/ui/games_lab.py +90 -0
- gini/ui/inspector.py +1055 -0
- gini/ui/live_metrics.py +130 -0
- gini/ui/machine_lab.py +1412 -0
- gini/ui/main_window.py +3153 -0
- gini/ui/memory_lab.py +371 -0
- gini/ui/mission_panel.py +302 -0
- gini/ui/mode_indicator.py +227 -0
- gini/ui/palette.py +112 -0
- gini/ui/peripherals.py +218 -0
- gini/ui/process_tree.py +130 -0
- gini/ui/reset_dialog.py +179 -0
- gini/ui/router_lab.py +776 -0
- gini/ui/run_button.py +183 -0
- gini/ui/settings_dialog.py +234 -0
- gini/ui/signin_dialog.py +111 -0
- gini/ui/storage_lab.py +219 -0
- gini/ui/syscall_builder.py +235 -0
- gini/ui/syscall_lab.py +152 -0
- gini/ui/theme/__init__.py +5 -0
- gini/ui/theme/icons.py +145 -0
- gini/ui/theme/manager.py +291 -0
- gini/ui/theme/tokens.py +194 -0
- gini/ui/trap_lab.py +270 -0
- gini/ui/worker_host.py +102 -0
- gini/ui/zoo_lab.py +112 -0
- gini_toolkit-6.0.1.dev0.dist-info/METADATA +77 -0
- gini_toolkit-6.0.1.dev0.dist-info/RECORD +278 -0
- gini_toolkit-6.0.1.dev0.dist-info/WHEEL +5 -0
- gini_toolkit-6.0.1.dev0.dist-info/entry_points.txt +3 -0
- gini_toolkit-6.0.1.dev0.dist-info/top_level.txt +1 -0
gini/ui/storage_lab.py
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Storage Lab — the visual file-system workbench for an xv6 Machine.
|
|
2
|
+
|
|
3
|
+
Reads the FS state (domain.xv6_fs) and shows it four ways: the on-disk region MAP (boot | super
|
|
4
|
+
| log | inodes | bitmap | data), the INODES and the directory tree they realize, the BUFFER
|
|
5
|
+
CACHE with its hit rate, and the write-ahead LOG — where you can Simulate a write and watch a
|
|
6
|
+
transaction fill, commit, and install, which is how xv6 survives a crash. Renders from an
|
|
7
|
+
injected provider (offline DemoDisk here; a GDB-backed reader on the Mac later).
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from PySide6.QtCore import Qt
|
|
12
|
+
from PySide6.QtGui import QColor, QPainter
|
|
13
|
+
from PySide6.QtWidgets import (
|
|
14
|
+
QDialog, QFrame, QGridLayout, QHBoxLayout, QHeaderView, QLabel, QPushButton, QTableWidget,
|
|
15
|
+
QTableWidgetItem, QVBoxLayout, QWidget,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from ..domain.xv6_fs import DemoDisk
|
|
19
|
+
from .theme import ThemeManager, icons
|
|
20
|
+
from .theme.manager import scale_css as _scss
|
|
21
|
+
|
|
22
|
+
_REGION_ACCENT = {"boot": "slate", "super": "blue", "log": "amber", "inodes": "green",
|
|
23
|
+
"bitmap": "purple", "data": "cyan"}
|
|
24
|
+
_ITYPE_ACCENT = {"dir": "blue", "file": "green", "dev": "amber", "free": "slate"}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DiskStrip(QWidget):
|
|
28
|
+
"""The on-disk region map — one labelled segment per region, widths weighted by block count
|
|
29
|
+
(with a floor so the tiny regions stay visible next to the huge data region)."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, theme) -> None:
|
|
32
|
+
super().__init__()
|
|
33
|
+
self.theme = theme
|
|
34
|
+
self._regions = []
|
|
35
|
+
self.setMinimumHeight(60)
|
|
36
|
+
|
|
37
|
+
def set_regions(self, regions) -> None:
|
|
38
|
+
self._regions = list(regions)
|
|
39
|
+
self.update()
|
|
40
|
+
|
|
41
|
+
def paintEvent(self, _e) -> None:
|
|
42
|
+
p = QPainter(self)
|
|
43
|
+
t = self.theme.theme
|
|
44
|
+
p.fillRect(self.rect(), QColor(t.panel2))
|
|
45
|
+
if not self._regions:
|
|
46
|
+
return
|
|
47
|
+
floor = 0.06
|
|
48
|
+
raw = [max(r.count, 1) for r in self._regions]
|
|
49
|
+
tot = sum(raw)
|
|
50
|
+
weights = [max(v / tot, floor) for v in raw]
|
|
51
|
+
wsum = sum(weights)
|
|
52
|
+
x = 0.0
|
|
53
|
+
W = self.width()
|
|
54
|
+
for r, w in zip(self._regions, weights):
|
|
55
|
+
seg = W * (w / wsum)
|
|
56
|
+
col = QColor(t.accent_for(_REGION_ACCENT.get(r.name, "slate")))
|
|
57
|
+
p.fillRect(int(x), 6, int(seg) - 2, self.height() - 28, col)
|
|
58
|
+
p.setPen(QColor(t.text))
|
|
59
|
+
p.drawText(int(x) + 4, 22, r.name)
|
|
60
|
+
p.setPen(QColor(t.muted))
|
|
61
|
+
rng = f"{r.start}" if r.count <= 1 else f"{r.start}–{r.end}"
|
|
62
|
+
p.drawText(int(x) + 4, self.height() - 8, rng)
|
|
63
|
+
x += seg
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class StorageLab(QDialog):
|
|
67
|
+
def __init__(self, parent, theme: ThemeManager, device=None, provider=None) -> None:
|
|
68
|
+
super().__init__(parent)
|
|
69
|
+
self.theme = theme
|
|
70
|
+
self.device = device
|
|
71
|
+
self.provider = provider or DemoDisk()
|
|
72
|
+
|
|
73
|
+
t = theme.theme
|
|
74
|
+
self.setWindowTitle(f"File System Lab — {getattr(device, 'name', 'xv6')}")
|
|
75
|
+
self.resize(920, 720)
|
|
76
|
+
self.setStyleSheet(f"QDialog{{background:{t.bg};}}")
|
|
77
|
+
root = QVBoxLayout(self)
|
|
78
|
+
self._build_header(root)
|
|
79
|
+
|
|
80
|
+
self._strip = DiskStrip(theme)
|
|
81
|
+
root.addWidget(self._panel("On-disk layout · block regions", self._strip))
|
|
82
|
+
|
|
83
|
+
grid = QGridLayout(); grid.setSpacing(10); root.addLayout(grid, 1)
|
|
84
|
+
self._inode_tbl = self._table(["inum", "type", "nlink", "size", "blocks"])
|
|
85
|
+
grid.addWidget(self._panel("Inodes", self._inode_tbl, fill=True), 0, 0)
|
|
86
|
+
self._tree_tbl = self._table(["", "inum", "name"])
|
|
87
|
+
grid.addWidget(self._panel("Directory tree", self._tree_tbl, fill=True), 0, 1)
|
|
88
|
+
self._buf_tbl = self._table(["block", "valid", "dirty", "ref"])
|
|
89
|
+
self._buf_panel = self._panel("Buffer cache", self._buf_tbl, fill=True)
|
|
90
|
+
grid.addWidget(self._buf_panel, 1, 0)
|
|
91
|
+
grid.addWidget(self._build_log_panel(), 1, 1)
|
|
92
|
+
grid.setColumnStretch(0, 1); grid.setColumnStretch(1, 1)
|
|
93
|
+
|
|
94
|
+
self._render(self.provider.snapshot())
|
|
95
|
+
|
|
96
|
+
# -- header/panels ---------------------------------------------------- #
|
|
97
|
+
def _build_header(self, root) -> None:
|
|
98
|
+
t = self.theme.theme
|
|
99
|
+
head = QHBoxLayout()
|
|
100
|
+
ic = QLabel(); ic.setPixmap(icons.render_pixmap("database", t.accent_for("cyan"), 22))
|
|
101
|
+
title = QLabel(f" File System Lab — {getattr(self.device, 'name', 'xv6')}")
|
|
102
|
+
title.setStyleSheet(_scss(f"color:{t.text};font-size:16px;font-weight:600;"))
|
|
103
|
+
head.addWidget(ic); head.addWidget(title); head.addStretch(1)
|
|
104
|
+
root.addLayout(head)
|
|
105
|
+
hint = QLabel("The on-disk regions, the inodes and the tree they build, the buffer "
|
|
106
|
+
"cache, and the write-ahead log that makes writes crash-safe.")
|
|
107
|
+
hint.setWordWrap(True); hint.setStyleSheet(_scss(f"color:{t.muted};font-size:11px;"))
|
|
108
|
+
root.addWidget(hint)
|
|
109
|
+
|
|
110
|
+
def _panel(self, title, inner, fill=False) -> QFrame:
|
|
111
|
+
t = self.theme.theme
|
|
112
|
+
f = QFrame(); f.setStyleSheet(
|
|
113
|
+
_scss(f"QFrame{{background:{t.panel2};border:1px solid {t.line};border-radius:10px;}}"))
|
|
114
|
+
v = QVBoxLayout(f); v.setContentsMargins(10, 8, 10, 10)
|
|
115
|
+
h = QLabel(title); h.setStyleSheet(
|
|
116
|
+
_scss(f"color:{t.muted};font-size:11px;font-weight:600;border:none;"))
|
|
117
|
+
v.addWidget(h)
|
|
118
|
+
inner.setStyleSheet((inner.styleSheet() or "") + "border:none;")
|
|
119
|
+
v.addWidget(inner, 1 if fill else 0)
|
|
120
|
+
return f
|
|
121
|
+
|
|
122
|
+
def _table(self, cols) -> QTableWidget:
|
|
123
|
+
t = self.theme.theme
|
|
124
|
+
tbl = QTableWidget(0, len(cols))
|
|
125
|
+
tbl.setHorizontalHeaderLabels(cols)
|
|
126
|
+
tbl.verticalHeader().setVisible(False)
|
|
127
|
+
tbl.setEditTriggers(QTableWidget.NoEditTriggers)
|
|
128
|
+
tbl.setSelectionMode(QTableWidget.NoSelection)
|
|
129
|
+
tbl.horizontalHeader().setSectionResizeMode(len(cols) - 1, QHeaderView.Stretch)
|
|
130
|
+
tbl.setStyleSheet(
|
|
131
|
+
f"QTableWidget{{background:{t.panel};color:{t.text};border:none;"
|
|
132
|
+
f"gridline-color:{t.line};font-size:12px;}}"
|
|
133
|
+
f"QHeaderView::section{{background:{t.panel2};color:{t.muted};border:none;"
|
|
134
|
+
"padding:4px;}")
|
|
135
|
+
return tbl
|
|
136
|
+
|
|
137
|
+
def _build_log_panel(self) -> QFrame:
|
|
138
|
+
t = self.theme.theme
|
|
139
|
+
f = QFrame(); f.setStyleSheet(
|
|
140
|
+
_scss(f"QFrame{{background:{t.panel2};border:1px solid {t.line};border-radius:10px;}}"))
|
|
141
|
+
v = QVBoxLayout(f); v.setContentsMargins(10, 8, 10, 10)
|
|
142
|
+
top = QHBoxLayout()
|
|
143
|
+
h = QLabel("Write-ahead log · journal"); h.setStyleSheet(
|
|
144
|
+
_scss(f"color:{t.muted};font-size:11px;font-weight:600;border:none;"))
|
|
145
|
+
top.addWidget(h); top.addStretch(1)
|
|
146
|
+
self._log_phase = QLabel(); self._log_phase.setStyleSheet(
|
|
147
|
+
f"color:{t.text};background:{t.panel};border:1px solid {t.line};"
|
|
148
|
+
"border-radius:9px;padding:2px 10px;font-size:11px;")
|
|
149
|
+
top.addWidget(self._log_phase)
|
|
150
|
+
v.addLayout(top)
|
|
151
|
+
self._log_body = QLabel(); self._log_body.setWordWrap(True); self._log_body.setAlignment(Qt.AlignTop)
|
|
152
|
+
self._log_body.setStyleSheet(_scss(f"color:{t.text};font-family:monospace;font-size:12px;border:none;"))
|
|
153
|
+
v.addWidget(self._log_body, 1)
|
|
154
|
+
live = not hasattr(self.provider, "simulate_write") # live reader can't fake a txn
|
|
155
|
+
btn = QPushButton(" Refresh" if live else " Simulate write")
|
|
156
|
+
btn.setToolTip("Re-read the live file-system state (launch `writer` from the scheduler "
|
|
157
|
+
"window to make real log transactions)" if live else
|
|
158
|
+
"Advance a simulated write-ahead-log transaction")
|
|
159
|
+
btn.setIcon(icons.icon("save", t.accent_for("green"), 14))
|
|
160
|
+
btn.clicked.connect(self._on_write)
|
|
161
|
+
btn.setStyleSheet(
|
|
162
|
+
f"QPushButton{{color:{t.text};background:{t.panel};border:1px solid {t.line};"
|
|
163
|
+
f"border-radius:8px;padding:6px 12px;}}QPushButton:hover{{border-color:{t.accent};}}")
|
|
164
|
+
v.addWidget(btn)
|
|
165
|
+
return f
|
|
166
|
+
|
|
167
|
+
# -- actions ---------------------------------------------------------- #
|
|
168
|
+
def _on_write(self) -> None:
|
|
169
|
+
fn = getattr(self.provider, "simulate_write", None)
|
|
170
|
+
self._render(fn() if callable(fn) else self.provider.snapshot())
|
|
171
|
+
|
|
172
|
+
def _render(self, snap) -> None:
|
|
173
|
+
t = self.theme.theme
|
|
174
|
+
self._strip.set_regions(snap.regions)
|
|
175
|
+
# inodes
|
|
176
|
+
self._inode_tbl.setRowCount(len(snap.inodes))
|
|
177
|
+
for r, ino in enumerate(snap.inodes):
|
|
178
|
+
vals = [str(ino.inum), ino.type, str(ino.nlink), str(ino.size), str(len(ino.blocks))]
|
|
179
|
+
for c, val in enumerate(vals):
|
|
180
|
+
it = QTableWidgetItem(val)
|
|
181
|
+
if c == 1:
|
|
182
|
+
it.setForeground(QColor(t.accent_for(_ITYPE_ACCENT.get(ino.type, "slate"))))
|
|
183
|
+
self._inode_tbl.setItem(r, c, it)
|
|
184
|
+
# tree
|
|
185
|
+
self._tree_tbl.setRowCount(len(snap.tree))
|
|
186
|
+
for r, d in enumerate(snap.tree):
|
|
187
|
+
leaf = d.name.split("/")[-1]
|
|
188
|
+
name = (" " * d.depth) + leaf + ("/" if d.is_dir else "")
|
|
189
|
+
for c, val in enumerate(["", str(d.inum), name]):
|
|
190
|
+
self._tree_tbl.setItem(r, c, QTableWidgetItem(val))
|
|
191
|
+
# buffer cache
|
|
192
|
+
self._buf_tbl.setRowCount(len(snap.bufs))
|
|
193
|
+
for r, b in enumerate(snap.bufs):
|
|
194
|
+
for c, val in enumerate([str(b.blockno), "✓" if b.valid else "",
|
|
195
|
+
"●" if b.dirty else "", str(b.refcnt)]):
|
|
196
|
+
it = QTableWidgetItem(val)
|
|
197
|
+
if c == 2 and b.dirty:
|
|
198
|
+
it.setForeground(QColor(t.accent_for("amber")))
|
|
199
|
+
self._buf_tbl.setItem(r, c, it)
|
|
200
|
+
self._buf_panel.findChild(QLabel).setText(
|
|
201
|
+
f"Buffer cache · {snap.hits} hits / {snap.misses} miss "
|
|
202
|
+
f"({snap.hit_rate * 100:.0f}% hit)")
|
|
203
|
+
# log
|
|
204
|
+
lg = snap.log
|
|
205
|
+
self._log_phase.setText(lg.phase)
|
|
206
|
+
col = {"idle": t.muted, "building": t.accent_for("amber"),
|
|
207
|
+
"committing": t.accent_for("green")}.get(lg.phase, t.muted)
|
|
208
|
+
self._log_phase.setStyleSheet(
|
|
209
|
+
f"color:{col};background:{t.panel};border:1px solid {t.line};"
|
|
210
|
+
"border-radius:9px;padding:2px 10px;font-size:11px;")
|
|
211
|
+
if lg.blocks:
|
|
212
|
+
self._log_body.setText(
|
|
213
|
+
f"transaction: {len(lg.blocks)} block(s) staged in the log\n"
|
|
214
|
+
f"dest blocks: {', '.join(map(str, lg.blocks))}\n"
|
|
215
|
+
+ ("committing → installing to their home locations…" if lg.committing
|
|
216
|
+
else "logged but not yet committed (a crash here loses nothing)."))
|
|
217
|
+
else:
|
|
218
|
+
self._log_body.setText("log idle — no transaction in flight.\n"
|
|
219
|
+
"Press “Simulate write” to stage one and watch it commit.")
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Syscall Builder — a visual front-end for adding a real system call to xv6.
|
|
2
|
+
|
|
3
|
+
The student names the call, declares its arguments and return type, and drops in a C body;
|
|
4
|
+
GINI generates the five real xv6 edits (see domain.syscall_builder) and previews them. Applying
|
|
5
|
+
writes the patches and recompiles xv6 — that step is the desktop (Mac) build; offline the dialog
|
|
6
|
+
still generates and previews the exact code, so the whole authoring experience is usable and
|
|
7
|
+
testable without a compiler.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from PySide6.QtCore import Qt
|
|
12
|
+
from PySide6.QtWidgets import (
|
|
13
|
+
QComboBox, QDialog, QFrame, QHBoxLayout, QHeaderView, QLabel, QLineEdit, QPlainTextEdit,
|
|
14
|
+
QPushButton, QTableWidget, QVBoxLayout, QWidget,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
from ..domain import syscall_builder as sb
|
|
18
|
+
from .theme import ThemeManager, icons
|
|
19
|
+
from .theme.manager import scale_css as _scss
|
|
20
|
+
|
|
21
|
+
_ARG_LABELS = [("int", "int"), ("pointer (addr)", "addr"), ("string", "str")]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SyscallBuilder(QDialog):
|
|
25
|
+
def __init__(self, parent, theme: ThemeManager, device=None, on_apply=None,
|
|
26
|
+
existing=None) -> None:
|
|
27
|
+
super().__init__(parent)
|
|
28
|
+
self.theme = theme
|
|
29
|
+
self.device = device
|
|
30
|
+
self.on_apply = on_apply # callable(Codegen) — Mac writes+recompiles
|
|
31
|
+
self._existing = tuple(existing or sb.STOCK_SYSCALLS)
|
|
32
|
+
self._added = 0 # syscalls authored this session (for numbering)
|
|
33
|
+
self._codegen = None
|
|
34
|
+
|
|
35
|
+
t = theme.theme
|
|
36
|
+
self.setWindowTitle("Syscall Builder — xv6")
|
|
37
|
+
self.resize(940, 680)
|
|
38
|
+
self.setStyleSheet(f"QDialog{{background:{t.bg};}}")
|
|
39
|
+
root = QVBoxLayout(self)
|
|
40
|
+
self._build_header(root)
|
|
41
|
+
body = QHBoxLayout(); root.addLayout(body, 1)
|
|
42
|
+
body.addWidget(self._build_form(), 0)
|
|
43
|
+
body.addWidget(self._build_preview(), 1)
|
|
44
|
+
self._build_footer(root)
|
|
45
|
+
self._add_arg_row("n", "int")
|
|
46
|
+
|
|
47
|
+
# -- header ----------------------------------------------------------- #
|
|
48
|
+
def _build_header(self, root) -> None:
|
|
49
|
+
t = self.theme.theme
|
|
50
|
+
head = QHBoxLayout()
|
|
51
|
+
ic = QLabel(); ic.setPixmap(icons.render_pixmap("compile", t.accent_for("red"), 22))
|
|
52
|
+
title = QLabel(" Add a system call to xv6")
|
|
53
|
+
title.setStyleSheet(_scss(f"color:{t.text};font-size:16px;font-weight:600;"))
|
|
54
|
+
head.addWidget(ic); head.addWidget(title); head.addStretch(1)
|
|
55
|
+
root.addLayout(head)
|
|
56
|
+
hint = QLabel("Declare it, write the C body, and GINI generates the five real xv6 "
|
|
57
|
+
"edits. Applying recompiles the kernel (desktop build).")
|
|
58
|
+
hint.setWordWrap(True); hint.setStyleSheet(_scss(f"color:{t.muted};font-size:11px;"))
|
|
59
|
+
root.addWidget(hint)
|
|
60
|
+
|
|
61
|
+
def _panel(self, title) -> tuple[QFrame, QVBoxLayout]:
|
|
62
|
+
t = self.theme.theme
|
|
63
|
+
f = QFrame(); f.setStyleSheet(
|
|
64
|
+
_scss(f"QFrame{{background:{t.panel2};border:1px solid {t.line};border-radius:10px;}}"))
|
|
65
|
+
v = QVBoxLayout(f); v.setContentsMargins(10, 8, 10, 10)
|
|
66
|
+
h = QLabel(title); h.setStyleSheet(
|
|
67
|
+
_scss(f"color:{t.muted};font-size:11px;font-weight:600;border:none;"))
|
|
68
|
+
v.addWidget(h)
|
|
69
|
+
return f, v
|
|
70
|
+
|
|
71
|
+
# -- the form --------------------------------------------------------- #
|
|
72
|
+
def _build_form(self) -> QFrame:
|
|
73
|
+
t = self.theme.theme
|
|
74
|
+
f, v = self._panel("Declaration")
|
|
75
|
+
f.setFixedWidth(380)
|
|
76
|
+
|
|
77
|
+
row = QHBoxLayout()
|
|
78
|
+
row.addWidget(self._lbl("name"))
|
|
79
|
+
self.name_edit = QLineEdit(); self.name_edit.setPlaceholderText("e.g. trace, hello")
|
|
80
|
+
self.name_edit.setStyleSheet(self._edit_css())
|
|
81
|
+
self.name_edit.textChanged.connect(self._on_name_changed)
|
|
82
|
+
row.addWidget(self.name_edit, 1)
|
|
83
|
+
row.addWidget(self._lbl("returns"))
|
|
84
|
+
self.ret_combo = QComboBox(); self.ret_combo.addItems(list(sb.RET_TYPES))
|
|
85
|
+
self.ret_combo.setStyleSheet(self._edit_css())
|
|
86
|
+
row.addWidget(self.ret_combo)
|
|
87
|
+
v.addLayout(row)
|
|
88
|
+
|
|
89
|
+
v.addWidget(self._lbl("arguments (up to 6)"))
|
|
90
|
+
self.args_tbl = QTableWidget(0, 2)
|
|
91
|
+
self.args_tbl.setHorizontalHeaderLabels(["name", "type"])
|
|
92
|
+
self.args_tbl.verticalHeader().setVisible(False)
|
|
93
|
+
self.args_tbl.horizontalHeader().setSectionResizeMode(0, QHeaderView.Stretch)
|
|
94
|
+
self.args_tbl.setStyleSheet(
|
|
95
|
+
f"QTableWidget{{background:{t.panel};color:{t.text};border:1px solid {t.line};"
|
|
96
|
+
f"gridline-color:{t.line};font-size:12px;}}"
|
|
97
|
+
f"QHeaderView::section{{background:{t.panel2};color:{t.muted};border:none;"
|
|
98
|
+
"padding:4px;}")
|
|
99
|
+
v.addWidget(self.args_tbl, 1)
|
|
100
|
+
arow = QHBoxLayout()
|
|
101
|
+
add = QPushButton(" + arg"); add.setStyleSheet(self._btn_css())
|
|
102
|
+
add.clicked.connect(lambda: self._add_arg_row())
|
|
103
|
+
rm = QPushButton(" − arg"); rm.setStyleSheet(self._btn_css())
|
|
104
|
+
rm.clicked.connect(self._remove_arg_row)
|
|
105
|
+
arow.addWidget(add); arow.addWidget(rm); arow.addStretch(1)
|
|
106
|
+
v.addLayout(arow)
|
|
107
|
+
|
|
108
|
+
v.addWidget(self._lbl("C body (runs in the kernel)"))
|
|
109
|
+
self.body_edit = QPlainTextEdit(sb.starter_body())
|
|
110
|
+
self.body_edit.setStyleSheet(
|
|
111
|
+
f"QPlainTextEdit{{background:{t.panel};color:{t.text};border:1px solid {t.line};"
|
|
112
|
+
"border-radius:6px;font-family:monospace;font-size:12px;}")
|
|
113
|
+
self.body_edit.setFixedHeight(150)
|
|
114
|
+
v.addWidget(self.body_edit)
|
|
115
|
+
return f
|
|
116
|
+
|
|
117
|
+
def _build_preview(self) -> QFrame:
|
|
118
|
+
t = self.theme.theme
|
|
119
|
+
f, v = self._panel("Generated xv6 edits (5 files)")
|
|
120
|
+
self.preview = QPlainTextEdit(); self.preview.setReadOnly(True)
|
|
121
|
+
self.preview.setPlaceholderText("Press Generate to see the code GINI will add.")
|
|
122
|
+
self.preview.setStyleSheet(
|
|
123
|
+
f"QPlainTextEdit{{background:{t.panel};color:{t.text};border:1px solid {t.line};"
|
|
124
|
+
"border-radius:6px;font-family:monospace;font-size:12px;}")
|
|
125
|
+
v.addWidget(self.preview, 1)
|
|
126
|
+
return f
|
|
127
|
+
|
|
128
|
+
def _build_footer(self, root) -> None:
|
|
129
|
+
t = self.theme.theme
|
|
130
|
+
bar = QHBoxLayout()
|
|
131
|
+
self.status = QLabel("")
|
|
132
|
+
self.status.setStyleSheet(_scss(f"color:{t.muted};font-size:12px;"))
|
|
133
|
+
bar.addWidget(self.status, 1)
|
|
134
|
+
gen = QPushButton(" Generate"); gen.setIcon(icons.icon("compile", t.accent_for("blue"), 14))
|
|
135
|
+
gen.setStyleSheet(self._btn_css()); gen.clicked.connect(self._on_generate)
|
|
136
|
+
bar.addWidget(gen)
|
|
137
|
+
self.apply_btn = QPushButton(" Apply & recompile")
|
|
138
|
+
self.apply_btn.setIcon(icons.icon("save", t.accent_for("green"), 14))
|
|
139
|
+
self.apply_btn.setStyleSheet(self._btn_css()); self.apply_btn.setEnabled(False)
|
|
140
|
+
self.apply_btn.clicked.connect(self._on_apply)
|
|
141
|
+
bar.addWidget(self.apply_btn)
|
|
142
|
+
root.addLayout(bar)
|
|
143
|
+
|
|
144
|
+
# -- helpers ---------------------------------------------------------- #
|
|
145
|
+
def _lbl(self, text) -> QLabel:
|
|
146
|
+
lb = QLabel(text)
|
|
147
|
+
lb.setStyleSheet(_scss(f"color:{self.theme.theme.muted};font-size:11px;border:none;"))
|
|
148
|
+
return lb
|
|
149
|
+
|
|
150
|
+
def _edit_css(self) -> str:
|
|
151
|
+
t = self.theme.theme
|
|
152
|
+
return (f"QLineEdit,QComboBox{{background:{t.panel};color:{t.text};"
|
|
153
|
+
f"border:1px solid {t.line};border-radius:6px;padding:4px 6px;}}")
|
|
154
|
+
|
|
155
|
+
def _btn_css(self) -> str:
|
|
156
|
+
t = self.theme.theme
|
|
157
|
+
return (f"QPushButton{{color:{t.text};background:{t.panel2};border:1px solid {t.line};"
|
|
158
|
+
f"border-radius:8px;padding:6px 12px;}}"
|
|
159
|
+
f"QPushButton:hover{{border-color:{t.accent};}}"
|
|
160
|
+
f"QPushButton:disabled{{color:{t.faint};}}")
|
|
161
|
+
|
|
162
|
+
def _add_arg_row(self, name="", typ="int") -> None:
|
|
163
|
+
if self.args_tbl.rowCount() >= 6:
|
|
164
|
+
return
|
|
165
|
+
r = self.args_tbl.rowCount()
|
|
166
|
+
self.args_tbl.insertRow(r)
|
|
167
|
+
edit = QLineEdit(name); edit.setStyleSheet(self._edit_css())
|
|
168
|
+
self.args_tbl.setCellWidget(r, 0, edit)
|
|
169
|
+
combo = QComboBox(); combo.setStyleSheet(self._edit_css())
|
|
170
|
+
for label, val in _ARG_LABELS:
|
|
171
|
+
combo.addItem(label, val)
|
|
172
|
+
combo.setCurrentIndex(next((i for i, (_l, v) in enumerate(_ARG_LABELS) if v == typ), 0))
|
|
173
|
+
self.args_tbl.setCellWidget(r, 1, combo)
|
|
174
|
+
|
|
175
|
+
def _remove_arg_row(self) -> None:
|
|
176
|
+
n = self.args_tbl.rowCount()
|
|
177
|
+
if n:
|
|
178
|
+
self.args_tbl.removeRow(n - 1)
|
|
179
|
+
|
|
180
|
+
def _on_name_changed(self, text) -> None:
|
|
181
|
+
# keep the starter body's printf in sync with the name until the user edits it
|
|
182
|
+
if self.body_edit.toPlainText().strip().startswith("// read your args"):
|
|
183
|
+
self.body_edit.setPlainText(sb.starter_body(text))
|
|
184
|
+
|
|
185
|
+
# -- spec + actions --------------------------------------------------- #
|
|
186
|
+
def spec_from_form(self) -> sb.SyscallSpec:
|
|
187
|
+
args = []
|
|
188
|
+
for r in range(self.args_tbl.rowCount()):
|
|
189
|
+
nm = self.args_tbl.cellWidget(r, 0).text().strip()
|
|
190
|
+
tp = self.args_tbl.cellWidget(r, 1).currentData()
|
|
191
|
+
if nm:
|
|
192
|
+
args.append(sb.SyscallArg(nm, tp))
|
|
193
|
+
return sb.SyscallSpec(name=self.name_edit.text().strip(),
|
|
194
|
+
ret=self.ret_combo.currentText(),
|
|
195
|
+
args=args, body=self.body_edit.toPlainText())
|
|
196
|
+
|
|
197
|
+
def _on_generate(self) -> None:
|
|
198
|
+
t = self.theme.theme
|
|
199
|
+
spec = self.spec_from_form()
|
|
200
|
+
errs = sb.validate(spec, existing=self._existing)
|
|
201
|
+
if errs:
|
|
202
|
+
self._codegen = None
|
|
203
|
+
self.apply_btn.setEnabled(False)
|
|
204
|
+
self.preview.setPlainText("")
|
|
205
|
+
self.status.setText("⚠ " + errs[0])
|
|
206
|
+
self.status.setStyleSheet(_scss(f"color:{t.danger};font-size:12px;"))
|
|
207
|
+
return
|
|
208
|
+
cg = sb.generate(spec, number=sb.NEXT_FREE_NUMBER + self._added)
|
|
209
|
+
self._codegen = cg
|
|
210
|
+
self.preview.setPlainText(cg.preview())
|
|
211
|
+
self.apply_btn.setEnabled(True)
|
|
212
|
+
self.status.setText(f"✓ Ready — SYS_{spec.name} = {cg.number}. "
|
|
213
|
+
"Review the 5 edits, then Apply.")
|
|
214
|
+
self.status.setStyleSheet(_scss(f"color:{t.success};font-size:12px;"))
|
|
215
|
+
|
|
216
|
+
def _on_apply(self) -> None:
|
|
217
|
+
t = self.theme.theme
|
|
218
|
+
if self._codegen is None:
|
|
219
|
+
return
|
|
220
|
+
if self.on_apply is not None:
|
|
221
|
+
try:
|
|
222
|
+
name = self.spec_from_form().name
|
|
223
|
+
self.on_apply(self._codegen)
|
|
224
|
+
self._added += 1
|
|
225
|
+
self._existing = self._existing + (name,) # block re-using this name
|
|
226
|
+
self.status.setText("Applied — writing the 5 edits and recompiling xv6…")
|
|
227
|
+
self.status.setStyleSheet(_scss(f"color:{t.success};font-size:12px;"))
|
|
228
|
+
self.apply_btn.setEnabled(False)
|
|
229
|
+
except Exception as e:
|
|
230
|
+
self.status.setText(f"⚠ Apply failed: {e}")
|
|
231
|
+
self.status.setStyleSheet(_scss(f"color:{t.danger};font-size:12px;"))
|
|
232
|
+
else:
|
|
233
|
+
self.status.setText("Applying writes these 5 edits and recompiles xv6 — available "
|
|
234
|
+
"on the desktop build. The generated code is shown at left.")
|
|
235
|
+
self.status.setStyleSheet(_scss(f"color:{t.muted};font-size:12px;"))
|
gini/ui/syscall_lab.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Syscall Lab — live histogram of system-call activity + an strace-style trace.
|
|
2
|
+
|
|
3
|
+
Reads gini_scdump over the serial (the agent's /sc, no gdb halt): per-syscall counters drive a
|
|
4
|
+
histogram of calls in the last 60 seconds (so you see what the machine is doing under the hood),
|
|
5
|
+
and a recent-call ring drives a running trace (`pid name(arg) = ret`). User-defined syscalls from
|
|
6
|
+
the Builder appear automatically, since counters are keyed by syscall number.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from PySide6.QtCore import Qt, QTimer, Signal
|
|
11
|
+
from PySide6.QtGui import QColor, QPainter
|
|
12
|
+
from PySide6.QtWidgets import (
|
|
13
|
+
QDialog, QFrame, QHBoxLayout, QLabel, QPlainTextEdit, QVBoxLayout, QWidget,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from ..domain.xv6 import SyscallRate, parse_sccounts, parse_sctrace, syscall_name
|
|
17
|
+
from .theme import ThemeManager, icons
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class HistogramBars(QWidget):
|
|
21
|
+
"""Horizontal bars for the top syscalls by call count in the window."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, theme) -> None:
|
|
24
|
+
super().__init__()
|
|
25
|
+
self.theme = theme
|
|
26
|
+
self._rows: list = [] # [(label, value)]
|
|
27
|
+
self.setMinimumHeight(220)
|
|
28
|
+
|
|
29
|
+
def set_data(self, rows) -> None:
|
|
30
|
+
self._rows = list(rows)
|
|
31
|
+
self.update()
|
|
32
|
+
|
|
33
|
+
def paintEvent(self, _e) -> None:
|
|
34
|
+
p = QPainter(self)
|
|
35
|
+
t = self.theme.theme
|
|
36
|
+
p.fillRect(self.rect(), QColor(t.panel))
|
|
37
|
+
if not self._rows:
|
|
38
|
+
p.setPen(QColor(t.muted))
|
|
39
|
+
p.drawText(self.rect(), Qt.AlignCenter, "no syscalls yet — launch a program")
|
|
40
|
+
return
|
|
41
|
+
top = self._rows[:12]
|
|
42
|
+
peak = max((v for _l, v in top), default=1) or 1
|
|
43
|
+
rowh = min(24, max(16, (self.height() - 8) // max(len(top), 1)))
|
|
44
|
+
label_w, count_w = 92, 52
|
|
45
|
+
bar_x = label_w
|
|
46
|
+
bar_max = max(40, self.width() - label_w - count_w - 12)
|
|
47
|
+
y = 4
|
|
48
|
+
for label, v in top:
|
|
49
|
+
p.setPen(QColor(t.text))
|
|
50
|
+
p.drawText(4, y, label_w - 6, rowh, Qt.AlignVCenter | Qt.AlignLeft, label)
|
|
51
|
+
w = int(bar_max * (v / peak))
|
|
52
|
+
p.fillRect(bar_x, y + 3, max(w, 1), rowh - 8, QColor(t.accent_for("blue")))
|
|
53
|
+
p.setPen(QColor(t.muted))
|
|
54
|
+
p.drawText(bar_x + bar_max + 6, y, count_w, rowh, Qt.AlignVCenter | Qt.AlignLeft,
|
|
55
|
+
str(v))
|
|
56
|
+
y += rowh
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class SyscallLab(QDialog):
|
|
60
|
+
sc_ready = Signal(str) # raw /sc text pushed from the poll worker
|
|
61
|
+
|
|
62
|
+
def __init__(self, parent, theme: ThemeManager, device=None, sc_source=None,
|
|
63
|
+
name_extra=None) -> None:
|
|
64
|
+
super().__init__(parent)
|
|
65
|
+
self.theme = theme
|
|
66
|
+
self.device = device
|
|
67
|
+
self._sc_source = sc_source or (lambda: "")
|
|
68
|
+
self._name_extra = name_extra or {}
|
|
69
|
+
self._rate = SyscallRate(window=60.0)
|
|
70
|
+
self._busy = False
|
|
71
|
+
|
|
72
|
+
t = theme.theme
|
|
73
|
+
self.setWindowTitle(f"System Calls Lab — {getattr(device, 'name', 'xv6')}")
|
|
74
|
+
self.resize(760, 560)
|
|
75
|
+
self.setStyleSheet(f"QDialog{{background:{t.bg};}}")
|
|
76
|
+
root = QVBoxLayout(self)
|
|
77
|
+
|
|
78
|
+
hdr = QHBoxLayout()
|
|
79
|
+
ic = QLabel(); ic.setPixmap(icons.render_pixmap("send", t.accent_for("blue"), 22))
|
|
80
|
+
title = QLabel(f" System Calls Lab — {getattr(device, 'name', 'xv6')}")
|
|
81
|
+
title.setStyleSheet(f"color:{t.text};font-size:16px;font-weight:600;")
|
|
82
|
+
hdr.addWidget(ic); hdr.addWidget(title); hdr.addStretch(1)
|
|
83
|
+
root.addLayout(hdr)
|
|
84
|
+
|
|
85
|
+
head = QLabel("What the kernel is doing under the hood — system calls in the last 60s "
|
|
86
|
+
"(histogram) and the most recent calls (trace). No gdb; live over serial.")
|
|
87
|
+
head.setWordWrap(True); head.setStyleSheet(f"color:{t.muted};font-size:12px;")
|
|
88
|
+
root.addWidget(head)
|
|
89
|
+
|
|
90
|
+
self._bars = HistogramBars(theme)
|
|
91
|
+
root.addWidget(self._panel("Syscalls · calls in last 60s", self._bars), 1)
|
|
92
|
+
|
|
93
|
+
self._trace = QPlainTextEdit(); self._trace.setReadOnly(True)
|
|
94
|
+
self._trace.setStyleSheet(
|
|
95
|
+
f"QPlainTextEdit{{background:{t.panel};color:{t.text};border:1px solid {t.line};"
|
|
96
|
+
"border-radius:6px;font-family:monospace;font-size:12px;}")
|
|
97
|
+
root.addWidget(self._panel("Trace · recent calls (pid name(arg) = ret)", self._trace), 1)
|
|
98
|
+
|
|
99
|
+
self.sc_ready.connect(self._apply)
|
|
100
|
+
self._poll = QTimer(self); self._poll.timeout.connect(self._fetch)
|
|
101
|
+
self._poll.start(1500)
|
|
102
|
+
self._fetch()
|
|
103
|
+
|
|
104
|
+
def _panel(self, title, inner) -> QFrame:
|
|
105
|
+
t = self.theme.theme
|
|
106
|
+
f = QFrame(); f.setStyleSheet(
|
|
107
|
+
f"QFrame{{background:{t.panel2};border:1px solid {t.line};border-radius:10px;}}")
|
|
108
|
+
v = QVBoxLayout(f); v.setContentsMargins(10, 8, 10, 10)
|
|
109
|
+
h = QLabel(title); h.setStyleSheet(
|
|
110
|
+
f"color:{t.muted};font-size:11px;font-weight:600;border:none;")
|
|
111
|
+
v.addWidget(h)
|
|
112
|
+
inner.setStyleSheet((inner.styleSheet() or "") + "border:none;")
|
|
113
|
+
v.addWidget(inner, 1)
|
|
114
|
+
return f
|
|
115
|
+
|
|
116
|
+
def _fetch(self) -> None:
|
|
117
|
+
if self._busy:
|
|
118
|
+
return
|
|
119
|
+
self._busy = True
|
|
120
|
+
import threading
|
|
121
|
+
|
|
122
|
+
def work():
|
|
123
|
+
try:
|
|
124
|
+
txt = self._sc_source()
|
|
125
|
+
except Exception:
|
|
126
|
+
txt = ""
|
|
127
|
+
self.sc_ready.emit(txt or "")
|
|
128
|
+
threading.Thread(target=work, daemon=True).start()
|
|
129
|
+
|
|
130
|
+
def _apply(self, txt) -> None:
|
|
131
|
+
self._busy = False
|
|
132
|
+
import time
|
|
133
|
+
counts = parse_sccounts(txt)
|
|
134
|
+
if counts:
|
|
135
|
+
self._rate.add(time.monotonic(), counts)
|
|
136
|
+
rates = self._rate.rates()
|
|
137
|
+
rows = sorted(((syscall_name(n, self._name_extra), v) for n, v in rates.items()),
|
|
138
|
+
key=lambda kv: -kv[1])
|
|
139
|
+
self._bars.set_data(rows)
|
|
140
|
+
evs = parse_sctrace(txt)
|
|
141
|
+
if evs:
|
|
142
|
+
lines = [f"{e.pid:>3} {syscall_name(e.num, self._name_extra)}({e.a0}) = {e.ret}"
|
|
143
|
+
for e in evs]
|
|
144
|
+
sb = self._trace.verticalScrollBar()
|
|
145
|
+
at_bottom = sb.value() >= sb.maximum() - 4
|
|
146
|
+
self._trace.setPlainText("\n".join(lines))
|
|
147
|
+
if at_bottom:
|
|
148
|
+
sb.setValue(sb.maximum())
|
|
149
|
+
|
|
150
|
+
def closeEvent(self, e) -> None: # noqa: N802
|
|
151
|
+
self._poll.stop()
|
|
152
|
+
super().closeEvent(e)
|