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/domain/xv6_fs.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""xv6 file-system state model — the Storage face's read side.
|
|
2
|
+
|
|
3
|
+
xv6's on-disk layout is a fixed sequence of regions (boot | super | log | inodes | bitmap |
|
|
4
|
+
data); a file is a `dinode` with up to 12 direct + 1 indirect block pointers; a directory is an
|
|
5
|
+
array of `dirent {inum, name}`; the buffer cache holds recently-used blocks; and crash-safe
|
|
6
|
+
writes go through a small write-ahead LOG (a transaction of block numbers that is committed then
|
|
7
|
+
installed). This module models all of that as pure data, with parsers for the GDB reads that
|
|
8
|
+
produce it (superblock, log header) — so the layout maths, the parsers, and the DemoDisk feed
|
|
9
|
+
are all unit-tested without a disk image. The live reads are Mac-side (GDB + xv6 symbols).
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
|
|
16
|
+
BSIZE = 1024 # xv6 block size
|
|
17
|
+
IPB = BSIZE // 64 # inodes per block (sizeof(dinode) == 64) -> 16
|
|
18
|
+
BPB = BSIZE * 8 # bitmap bits per block -> 8192
|
|
19
|
+
DIRSIZ = 14
|
|
20
|
+
_ITYPE = {0: "free", 1: "dir", 2: "file", 3: "dev"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class Superblock:
|
|
25
|
+
size: int = 0 # total blocks
|
|
26
|
+
nblocks: int = 0 # data blocks
|
|
27
|
+
ninodes: int = 0
|
|
28
|
+
nlog: int = 0
|
|
29
|
+
logstart: int = 0
|
|
30
|
+
inodestart: int = 0
|
|
31
|
+
bmapstart: int = 0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class Region:
|
|
36
|
+
name: str
|
|
37
|
+
start: int
|
|
38
|
+
count: int
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def end(self) -> int:
|
|
42
|
+
return self.start + max(self.count, 1) - 1
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class Inode:
|
|
47
|
+
inum: int
|
|
48
|
+
type: str # free|dir|file|dev
|
|
49
|
+
nlink: int = 0
|
|
50
|
+
size: int = 0
|
|
51
|
+
blocks: list = field(default_factory=list) # data block numbers (direct+indirect)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class Dirent:
|
|
56
|
+
inum: int
|
|
57
|
+
name: str
|
|
58
|
+
is_dir: bool = False
|
|
59
|
+
depth: int = 0 # for a flattened tree view
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class Buf:
|
|
64
|
+
blockno: int
|
|
65
|
+
valid: bool = True
|
|
66
|
+
dirty: bool = False
|
|
67
|
+
refcnt: int = 0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class LogState:
|
|
72
|
+
start: int = 0
|
|
73
|
+
size: int = 0
|
|
74
|
+
outstanding: int = 0 # syscalls in flight in this group commit
|
|
75
|
+
committing: bool = False
|
|
76
|
+
blocks: list = field(default_factory=list) # dest block numbers in the current transaction
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def phase(self) -> str:
|
|
80
|
+
if self.committing:
|
|
81
|
+
return "committing"
|
|
82
|
+
if self.blocks:
|
|
83
|
+
return "building"
|
|
84
|
+
return "idle"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class FsSnapshot:
|
|
89
|
+
sb: Superblock
|
|
90
|
+
regions: list = field(default_factory=list)
|
|
91
|
+
inodes: list = field(default_factory=list)
|
|
92
|
+
tree: list = field(default_factory=list) # flattened Dirents
|
|
93
|
+
bufs: list = field(default_factory=list)
|
|
94
|
+
log: LogState = field(default_factory=LogState)
|
|
95
|
+
hits: int = 0
|
|
96
|
+
misses: int = 0
|
|
97
|
+
# Provenance so the UI never passes off demo data as real. `source` is "real" (from the
|
|
98
|
+
# running kernel) or "demo" (the DemoDisk stand-in). `ok` is False when a REAL read was
|
|
99
|
+
# attempted but produced nothing — the face must show an error, NOT silently fall to demo.
|
|
100
|
+
source: str = "real"
|
|
101
|
+
ok: bool = True
|
|
102
|
+
# Which real panels this build can actually populate (the rest render "not available (real)"
|
|
103
|
+
# instead of fake data). Superblock + log are dumped today; inodes/dir/bcache are not yet.
|
|
104
|
+
have: tuple = ("layout", "log")
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def hit_rate(self) -> float:
|
|
108
|
+
tot = self.hits + self.misses
|
|
109
|
+
return (self.hits / tot) if tot else 0.0
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# -- layout maths ----------------------------------------------------------- #
|
|
113
|
+
def layout(sb: Superblock) -> list[Region]:
|
|
114
|
+
"""The fixed on-disk region sequence, derived from the superblock."""
|
|
115
|
+
ninode_blocks = (sb.ninodes + IPB - 1) // IPB if sb.ninodes else 0
|
|
116
|
+
nbitmap = (sb.size + BPB - 1) // BPB if sb.size else 0
|
|
117
|
+
data_start = sb.bmapstart + nbitmap
|
|
118
|
+
data_blocks = max(sb.size - data_start, 0)
|
|
119
|
+
return [
|
|
120
|
+
Region("boot", 0, 1),
|
|
121
|
+
Region("super", 1, 1),
|
|
122
|
+
Region("log", sb.logstart, sb.nlog),
|
|
123
|
+
Region("inodes", sb.inodestart, ninode_blocks),
|
|
124
|
+
Region("bitmap", sb.bmapstart, nbitmap),
|
|
125
|
+
Region("data", data_start, data_blocks),
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# -- parsers (GDB struct prints) -------------------------------------------- #
|
|
130
|
+
def _ints(text: str) -> list[int]:
|
|
131
|
+
"""All integers in a GDB brace list, expanding `N <repeats K times>`."""
|
|
132
|
+
out: list[int] = []
|
|
133
|
+
for tok in text.split(","):
|
|
134
|
+
tok = tok.strip()
|
|
135
|
+
m = re.match(r"(-?\d+)\s*<repeats\s+(\d+)\s+times>", tok)
|
|
136
|
+
if m:
|
|
137
|
+
out += [int(m.group(1))] * int(m.group(2))
|
|
138
|
+
elif re.match(r"-?\d+$", tok):
|
|
139
|
+
out.append(int(tok))
|
|
140
|
+
return out
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _field(text: str, name: str) -> int | None:
|
|
144
|
+
m = re.search(rf"\b{name}\s*=\s*(-?\d+)", text)
|
|
145
|
+
return int(m.group(1)) if m else None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def parse_superblock(text: str) -> Superblock:
|
|
149
|
+
"""GDB `p *sb` -> Superblock (fields matched by name; magic/order-independent)."""
|
|
150
|
+
g = lambda n: _field(text, n) or 0
|
|
151
|
+
return Superblock(size=g("size"), nblocks=g("nblocks"), ninodes=g("ninodes"),
|
|
152
|
+
nlog=g("nlog"), logstart=g("logstart"), inodestart=g("inodestart"),
|
|
153
|
+
bmapstart=g("bmapstart"))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def parse_logheader(text: str, start: int = 0, size: int = 0,
|
|
157
|
+
outstanding: int = 0, committing: bool = False) -> LogState:
|
|
158
|
+
"""Parse the write-ahead log state. `n` = blocks in this transaction; `block[]` = dests.
|
|
159
|
+
Also reads `start`/`outstanding`/`committing` from the serial dump if present (the `\\bstart`
|
|
160
|
+
boundary avoids matching the superblock's logstart/inodestart/bmapstart)."""
|
|
161
|
+
n = _field(text, "n") or 0
|
|
162
|
+
m = re.search(r"block\s*=\s*\{([^}]*)\}", text)
|
|
163
|
+
blocks = _ints(m.group(1))[:n] if m else []
|
|
164
|
+
st, out, com = _field(text, "start"), _field(text, "outstanding"), _field(text, "committing")
|
|
165
|
+
return LogState(start=st if st is not None else start, size=size,
|
|
166
|
+
outstanding=out if out is not None else outstanding,
|
|
167
|
+
committing=bool(com) if com is not None else committing, blocks=blocks)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def parse_dinode(text: str, inum: int) -> Inode:
|
|
171
|
+
"""GDB `p *ip` (dinode) -> Inode (type/nlink/size + addrs block list)."""
|
|
172
|
+
t = _ITYPE.get(_field(text, "type") or 0, "free")
|
|
173
|
+
m = re.search(r"addrs\s*=\s*\{([^}]*)\}", text)
|
|
174
|
+
blocks = [b for b in _ints(m.group(1))] if m else []
|
|
175
|
+
return Inode(inum=inum, type=t, nlink=_field(text, "nlink") or 0,
|
|
176
|
+
size=_field(text, "size") or 0, blocks=[b for b in blocks if b])
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def fs_summary(fs: FsSnapshot) -> str:
|
|
180
|
+
"""Compact, LLM-facing summary of the file-system state (for the Ask GINI card, L2)."""
|
|
181
|
+
if fs is None:
|
|
182
|
+
return ""
|
|
183
|
+
sb = fs.sb
|
|
184
|
+
lines = [f"file system: {sb.size} blocks · layout "
|
|
185
|
+
+ " | ".join(r.name for r in fs.regions)]
|
|
186
|
+
active = [i for i in fs.inodes if i.type != "free"]
|
|
187
|
+
if active:
|
|
188
|
+
lines.append(" inodes: "
|
|
189
|
+
+ ", ".join(f"#{i.inum} {i.type}({i.size}B)" for i in active[:6]))
|
|
190
|
+
lines.append(f" buffer cache: {fs.hits} hits / {fs.misses} miss "
|
|
191
|
+
f"({fs.hit_rate * 100:.0f}% hit)")
|
|
192
|
+
lg = fs.log
|
|
193
|
+
tail = (f" — {len(lg.blocks)} block(s): {', '.join(map(str, lg.blocks))}"
|
|
194
|
+
if lg.blocks else "")
|
|
195
|
+
lines.append(f" write-ahead log: {lg.phase}{tail}")
|
|
196
|
+
return "\n".join(lines)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# -- offline demo ----------------------------------------------------------- #
|
|
200
|
+
class DemoDisk:
|
|
201
|
+
"""A deterministic small xv6 file system + a simulated write, so the Storage face is
|
|
202
|
+
explorable and testable without a disk image. Not a real FS — a faithful-shaped stand-in."""
|
|
203
|
+
|
|
204
|
+
def __init__(self) -> None:
|
|
205
|
+
self.sb = Superblock(size=2000, nblocks=1954, ninodes=200, nlog=30,
|
|
206
|
+
logstart=2, inodestart=32, bmapstart=45)
|
|
207
|
+
self._regions = layout(self.sb)
|
|
208
|
+
data0 = self._regions[-1].start
|
|
209
|
+
self.inodes = [
|
|
210
|
+
Inode(1, "dir", 1, 96, [data0]),
|
|
211
|
+
Inode(2, "dev", 1, 0, []),
|
|
212
|
+
Inode(3, "file", 1, 2200, [data0 + 1, data0 + 2, data0 + 3]),
|
|
213
|
+
Inode(4, "dir", 1, 64, [data0 + 4]),
|
|
214
|
+
Inode(5, "file", 2, 900, [data0 + 5]),
|
|
215
|
+
]
|
|
216
|
+
self.tree = [
|
|
217
|
+
Dirent(1, "/", True, 0),
|
|
218
|
+
Dirent(2, "console", False, 1),
|
|
219
|
+
Dirent(3, "README", False, 1),
|
|
220
|
+
Dirent(4, "usr", True, 1),
|
|
221
|
+
Dirent(5, "usr/sh", False, 2),
|
|
222
|
+
]
|
|
223
|
+
self.bufs = [Buf(1, True, False, 0), Buf(32, True, False, 1),
|
|
224
|
+
Buf(data0, True, False, 2), Buf(data0 + 1, True, True, 1),
|
|
225
|
+
Buf(45, True, False, 0)]
|
|
226
|
+
self.hits, self.misses = 128, 24
|
|
227
|
+
self.log = LogState(start=self.sb.logstart, size=self.sb.nlog)
|
|
228
|
+
|
|
229
|
+
def snapshot(self) -> FsSnapshot:
|
|
230
|
+
return FsSnapshot(sb=self.sb, regions=list(self._regions), inodes=list(self.inodes),
|
|
231
|
+
tree=list(self.tree), bufs=list(self.bufs), log=self.log,
|
|
232
|
+
hits=self.hits, misses=self.misses, source="demo",
|
|
233
|
+
have=("layout", "log", "inodes", "dir", "bcache"))
|
|
234
|
+
|
|
235
|
+
def simulate_write(self) -> FsSnapshot:
|
|
236
|
+
"""Advance the write-ahead log: idle -> building -> committing -> installed(idle)."""
|
|
237
|
+
d0 = self._regions[-1].start
|
|
238
|
+
if self.log.phase == "idle":
|
|
239
|
+
self.log = LogState(self.sb.logstart, self.sb.nlog, outstanding=1,
|
|
240
|
+
blocks=[45, d0 + 1, self.sb.inodestart]) # bitmap, data, inode
|
|
241
|
+
self.hits += 3
|
|
242
|
+
elif self.log.phase == "building":
|
|
243
|
+
self.log = LogState(self.sb.logstart, self.sb.nlog, committing=True,
|
|
244
|
+
blocks=list(self.log.blocks))
|
|
245
|
+
else: # committing -> installed, back to idle
|
|
246
|
+
for b in self.log.blocks:
|
|
247
|
+
self.bufs.append(Buf(b, True, False, 0))
|
|
248
|
+
self.misses += 1
|
|
249
|
+
self.log = LogState(self.sb.logstart, self.sb.nlog)
|
|
250
|
+
return self.snapshot()
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Xv6Runner — the OS domain's behavioral-probe runner (the GINI-as-oracle seam for the kernel).
|
|
2
|
+
|
|
3
|
+
The networking oracle answers `reach(A -> B) == ok` against a live topology. The OS oracle answers
|
|
4
|
+
`measure(<what>, <which>) <op> <value>` against the running kernel's telemetry — CPU share, wait
|
|
5
|
+
times, ticket fairness, shadow liveness. Crucially this reuses the EXISTING probe grammar and
|
|
6
|
+
evaluator in `probes.py` unchanged: we only add a runner that knows how to compute OS metrics, the
|
|
7
|
+
same way `services/probe_runner.py` computes networking ones. GINI, never the model, gives the
|
|
8
|
+
verdict.
|
|
9
|
+
|
|
10
|
+
Pure: it reads a `Snapshot` (procs + sched fields), a `SchedTimeline` (who ran, CPU share), and the
|
|
11
|
+
shadow manifest ({name: ShadowStatus}); a `FakeRunner`/hand-built telemetry makes every predicate
|
|
12
|
+
unit-testable offline.
|
|
13
|
+
|
|
14
|
+
OS metrics understood by `measure(what, which)`:
|
|
15
|
+
cpu_share, highest_priority -> the highest-priority proc's fraction of recent CPU
|
|
16
|
+
cpu_share, <pid> -> that pid's fraction of recent CPU
|
|
17
|
+
max_wait_slices, any -> longest any RUNNABLE proc has waited (aging counter)
|
|
18
|
+
share_ratio, tickets -> max deviation of observed share from the ticket ratio
|
|
19
|
+
every_runnable_runs, any -> 1.0 if every runnable proc ran in the window, else 0.0
|
|
20
|
+
shadow_active, <name> -> 1.0 if that shadow is active, else 0.0
|
|
21
|
+
shadow_faults, <name> -> how many times that shadow crashed back to the primary
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Xv6Runner:
|
|
27
|
+
"""A `probes.Runner` for the OS domain. Only `available()` + `measure()` are meaningful here
|
|
28
|
+
(OS win-conditions are all `measure(...)`); the networking methods are harmless no-ops so the
|
|
29
|
+
same evaluator can call either runner."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, snapshot=None, timeline=None, shadows=None, window: int = 60) -> None:
|
|
32
|
+
self.snapshot = snapshot # domain.xv6.Snapshot (procs + sched fields)
|
|
33
|
+
self.timeline = timeline # domain.xv6.SchedTimeline (who ran / shares)
|
|
34
|
+
self.shadows = shadows or {} # {name: ShadowStatus}
|
|
35
|
+
self.window = window
|
|
36
|
+
|
|
37
|
+
# -- probes.Runner protocol --------------------------------------------- #
|
|
38
|
+
def available(self) -> bool:
|
|
39
|
+
return self.snapshot is not None
|
|
40
|
+
|
|
41
|
+
def reach(self, src, dst, port=None) -> bool: # not used by OS probes
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
def http(self, src, dst, port) -> bool:
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
def backends(self, lb) -> int:
|
|
48
|
+
return 0
|
|
49
|
+
|
|
50
|
+
def flow(self, ovs, match) -> bool:
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
def measure(self, what: str, which: str):
|
|
54
|
+
"""Return the OS metric value, or None when it can't be read (probe treats None as
|
|
55
|
+
not-satisfied, never a crash)."""
|
|
56
|
+
if self.snapshot is None:
|
|
57
|
+
return None
|
|
58
|
+
procs = list(getattr(self.snapshot, "procs", []) or [])
|
|
59
|
+
handler = getattr(self, f"_m_{what}", None)
|
|
60
|
+
return handler(which, procs) if handler else None
|
|
61
|
+
|
|
62
|
+
# -- individual metrics -------------------------------------------------- #
|
|
63
|
+
def _shares(self) -> dict:
|
|
64
|
+
return self.timeline.shares(self.window) if self.timeline is not None else {}
|
|
65
|
+
|
|
66
|
+
def _highest_priority_pid(self, procs):
|
|
67
|
+
"""The proc with the numerically-lowest priority = highest scheduling priority."""
|
|
68
|
+
cand = [p for p in procs if p.priority is not None]
|
|
69
|
+
if not cand:
|
|
70
|
+
return None
|
|
71
|
+
return min(cand, key=lambda p: (p.priority, p.pid)).pid
|
|
72
|
+
|
|
73
|
+
def _m_cpu_share(self, which, procs):
|
|
74
|
+
shares = self._shares()
|
|
75
|
+
if which == "highest_priority":
|
|
76
|
+
pid = self._highest_priority_pid(procs)
|
|
77
|
+
return None if pid is None else float(shares.get(pid, 0.0))
|
|
78
|
+
try:
|
|
79
|
+
return float(shares.get(int(which), 0.0))
|
|
80
|
+
except (TypeError, ValueError):
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
def _m_max_wait_slices(self, which, procs):
|
|
84
|
+
waits = [p.wait_ticks for p in procs
|
|
85
|
+
if p.state == "runnable" and p.wait_ticks is not None]
|
|
86
|
+
return float(max(waits)) if waits else None
|
|
87
|
+
|
|
88
|
+
def _m_share_ratio(self, which, procs):
|
|
89
|
+
"""Max absolute deviation of each ticketed proc's observed CPU share from its ticket share
|
|
90
|
+
(0 = share tracks tickets exactly; the lottery win-condition asserts this is small)."""
|
|
91
|
+
ticketed = [p for p in procs if p.tickets]
|
|
92
|
+
total = sum(p.tickets for p in ticketed)
|
|
93
|
+
if not ticketed or total <= 0:
|
|
94
|
+
return None
|
|
95
|
+
shares = self._shares()
|
|
96
|
+
return float(max(abs(shares.get(p.pid, 0.0) - p.tickets / total) for p in ticketed))
|
|
97
|
+
|
|
98
|
+
def _m_every_runnable_runs(self, which, procs):
|
|
99
|
+
runnable = {p.pid for p in procs if p.state in ("runnable", "running")}
|
|
100
|
+
if not runnable:
|
|
101
|
+
return 1.0
|
|
102
|
+
if self.timeline is None:
|
|
103
|
+
return None
|
|
104
|
+
ran = {s.pid for s in self.timeline.recent(self.window)}
|
|
105
|
+
return 1.0 if runnable <= ran else 0.0
|
|
106
|
+
|
|
107
|
+
def _m_shadow_active(self, which, procs):
|
|
108
|
+
s = self.shadows.get(which)
|
|
109
|
+
return 1.0 if (s is not None and s.active) else 0.0
|
|
110
|
+
|
|
111
|
+
def _m_shadow_faults(self, which, procs):
|
|
112
|
+
s = self.shadows.get(which)
|
|
113
|
+
return None if s is None else float(s.faults)
|