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
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Long-running Source/Sink execution — start, stream, and cleanly stop a rider on its donor.
|
|
2
|
+
|
|
3
|
+
Unlike the one-shot `RiderRunner` (used for deterministic grading), a rider in the app runs
|
|
4
|
+
CONTINUOUSLY: start its tool inside the donor, stream output live, stop on demand. Two things make
|
|
5
|
+
this safe:
|
|
6
|
+
|
|
7
|
+
* **streaming** — a reader thread reads the tool's stdout line by line and pushes a rolling
|
|
8
|
+
{raw, measurement, running} snapshot to an `on_update` callback (the caller marshals it to the
|
|
9
|
+
GUI via a Qt signal);
|
|
10
|
+
* **clean teardown** — `docker exec` does not reliably kill the in-container process when the
|
|
11
|
+
client goes away, so each rider is launched via a tiny wrapper that writes its PID to a file;
|
|
12
|
+
`stop()` kills that PID inside the container, then terminates the local client. No orphans.
|
|
13
|
+
|
|
14
|
+
Popen + the kill call are injectable so the whole lifecycle is unit-testable without Docker.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import shlex
|
|
19
|
+
import subprocess
|
|
20
|
+
import threading
|
|
21
|
+
|
|
22
|
+
from ..domain import riders as _riders
|
|
23
|
+
|
|
24
|
+
# donor neighbours a ping/http rider can auto-target (reachable by service name)
|
|
25
|
+
_TARGETABLE = {"host", "router", "instance", "container", "kinstance", "web_app", "firewall"}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _Sess:
|
|
29
|
+
def __init__(self, rider_id, type_key, service, pidfile, proc, donor, inferred) -> None:
|
|
30
|
+
self.rider_id = rider_id
|
|
31
|
+
self.type_key = type_key
|
|
32
|
+
self.service = service
|
|
33
|
+
self.pidfile = pidfile
|
|
34
|
+
self.proc = proc
|
|
35
|
+
self.donor = donor
|
|
36
|
+
self.inferred = inferred
|
|
37
|
+
self.lines: list[str] = []
|
|
38
|
+
self.stop_flag = False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RiderSessions:
|
|
42
|
+
def __init__(self, orchestrator, *, popen_factory=None, timeout: float = 8.0) -> None:
|
|
43
|
+
self.orch = orchestrator
|
|
44
|
+
self._popen = popen_factory or subprocess.Popen
|
|
45
|
+
self.timeout = timeout
|
|
46
|
+
self._sessions: dict[str, _Sess] = {}
|
|
47
|
+
self._svc_cache: dict | None = None
|
|
48
|
+
|
|
49
|
+
# -- state -------------------------------------------------------------- #
|
|
50
|
+
def is_running(self, rider_id: str) -> bool:
|
|
51
|
+
return rider_id in self._sessions
|
|
52
|
+
|
|
53
|
+
def running_ids(self) -> list[str]:
|
|
54
|
+
return list(self._sessions)
|
|
55
|
+
|
|
56
|
+
def available(self) -> bool:
|
|
57
|
+
try:
|
|
58
|
+
states = self.orch.status()
|
|
59
|
+
except Exception:
|
|
60
|
+
return False
|
|
61
|
+
return any(str(s).startswith("running") for s in (states or {}).values())
|
|
62
|
+
|
|
63
|
+
def _service(self, name: str) -> str:
|
|
64
|
+
if self._svc_cache is None:
|
|
65
|
+
try:
|
|
66
|
+
self._svc_cache = {k.lower(): k for k in (self.orch.status() or {})}
|
|
67
|
+
except Exception:
|
|
68
|
+
self._svc_cache = {}
|
|
69
|
+
return self._svc_cache.get(name.lower(), name.lower())
|
|
70
|
+
|
|
71
|
+
def _infer_target(self, topo, donor_id: str, rider_id: str) -> str:
|
|
72
|
+
for nb in topo.neighbors(donor_id):
|
|
73
|
+
if nb.id != rider_id and nb.type_key in _TARGETABLE:
|
|
74
|
+
return nb.name
|
|
75
|
+
for d in topo.devices.values():
|
|
76
|
+
if d.id not in (donor_id, rider_id) and d.type_key in _TARGETABLE:
|
|
77
|
+
return d.name
|
|
78
|
+
return ""
|
|
79
|
+
|
|
80
|
+
# -- lifecycle ---------------------------------------------------------- #
|
|
81
|
+
def start(self, topo, rider_id: str, on_update) -> dict:
|
|
82
|
+
"""Launch the rider on its donor and stream updates. Returns a start result dict."""
|
|
83
|
+
if rider_id in self._sessions:
|
|
84
|
+
return {"ok": True, "running": True, "already": True}
|
|
85
|
+
rider = topo.devices.get(rider_id)
|
|
86
|
+
if rider is None:
|
|
87
|
+
return {"ok": False, "error": "no such rider"}
|
|
88
|
+
donor = topo.donor_of(rider_id)
|
|
89
|
+
if donor is None:
|
|
90
|
+
return {"ok": False, "error": f"{rider.name} isn't attached to a donor yet."}
|
|
91
|
+
|
|
92
|
+
props = dict(rider.properties)
|
|
93
|
+
inferred = ""
|
|
94
|
+
if rider.type_key in ("ping_probe", "http_probe") and not (props.get("Target") or "").strip():
|
|
95
|
+
inferred = self._infer_target(topo, donor.id, rider_id)
|
|
96
|
+
if inferred:
|
|
97
|
+
props["Target"] = inferred
|
|
98
|
+
try:
|
|
99
|
+
argv = _riders.build_command(rider.type_key, props)
|
|
100
|
+
except _riders.RiderError as e:
|
|
101
|
+
return {"ok": False, "error": str(e)}
|
|
102
|
+
|
|
103
|
+
service = self._service(donor.name)
|
|
104
|
+
pidfile = f"/tmp/gini-rider-{rider_id}.pid"
|
|
105
|
+
# write our PID, then exec the tool (so `stop` can kill exactly this process in the container)
|
|
106
|
+
wrapped = ["sh", "-lc", f"echo $$ > {pidfile}; exec {shlex.join(argv)}"]
|
|
107
|
+
cmd = [*self.orch._dc, "exec", "-T", service, *wrapped]
|
|
108
|
+
wd = getattr(self.orch, "workdir", None)
|
|
109
|
+
try:
|
|
110
|
+
proc = self._popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
|
111
|
+
text=True, bufsize=1, cwd=str(wd) if wd else None)
|
|
112
|
+
except (FileNotFoundError, OSError) as e:
|
|
113
|
+
return {"ok": False, "error": f"couldn't start: {e}"}
|
|
114
|
+
|
|
115
|
+
sess = _Sess(rider_id, rider.type_key, service, pidfile, proc, donor.name, inferred)
|
|
116
|
+
self._sessions[rider_id] = sess
|
|
117
|
+
threading.Thread(target=self._reader, args=(sess, on_update), daemon=True).start()
|
|
118
|
+
return {"ok": True, "running": True, "donor": donor.name, "inferred_target": inferred}
|
|
119
|
+
|
|
120
|
+
def _snapshot(self, sess: _Sess, running: bool) -> dict:
|
|
121
|
+
raw = "\n".join(sess.lines)
|
|
122
|
+
m = _riders.parse_measurement(sess.type_key, raw)
|
|
123
|
+
return {"ok": True, "running": running, "raw": raw, "measurement": m,
|
|
124
|
+
"summary": _riders.summarize(sess.type_key, m), "donor": sess.donor,
|
|
125
|
+
"inferred_target": sess.inferred}
|
|
126
|
+
|
|
127
|
+
def _reader(self, sess: _Sess, on_update) -> None:
|
|
128
|
+
import time
|
|
129
|
+
last = 0.0
|
|
130
|
+
try:
|
|
131
|
+
for line in iter(sess.proc.stdout.readline, ""):
|
|
132
|
+
if sess.stop_flag:
|
|
133
|
+
break
|
|
134
|
+
sess.lines.append(line.rstrip("\n"))
|
|
135
|
+
if len(sess.lines) > 300:
|
|
136
|
+
sess.lines = sess.lines[-300:]
|
|
137
|
+
# THROTTLE: a high-rate rider (tcpdump under load prints per packet) would flood the
|
|
138
|
+
# GUI with rider_ran signals and freeze it. Coalesce to ~4 updates/sec; the final
|
|
139
|
+
# snapshot below always fires, so no data is lost — only intermediate redraws.
|
|
140
|
+
now = time.monotonic()
|
|
141
|
+
if now - last >= 0.25:
|
|
142
|
+
on_update(sess.rider_id, self._snapshot(sess, running=True))
|
|
143
|
+
last = now
|
|
144
|
+
except Exception: # noqa: BLE001 — a dead pipe just ends the run
|
|
145
|
+
pass
|
|
146
|
+
self._sessions.pop(sess.rider_id, None) # finite run finished, or we were stopped
|
|
147
|
+
on_update(sess.rider_id, self._snapshot(sess, running=False))
|
|
148
|
+
|
|
149
|
+
def stop(self, rider_id: str) -> None:
|
|
150
|
+
sess = self._sessions.get(rider_id)
|
|
151
|
+
if sess is None:
|
|
152
|
+
return
|
|
153
|
+
sess.stop_flag = True
|
|
154
|
+
wd = getattr(self.orch, "workdir", None)
|
|
155
|
+
try: # kill the tool INSIDE the container
|
|
156
|
+
subprocess.run(
|
|
157
|
+
[*self.orch._dc, "exec", "-T", sess.service, "sh", "-lc",
|
|
158
|
+
f"kill $(cat {sess.pidfile}) 2>/dev/null; rm -f {sess.pidfile}"],
|
|
159
|
+
cwd=str(wd) if wd else None, capture_output=True, timeout=self.timeout)
|
|
160
|
+
except Exception: # noqa: BLE001
|
|
161
|
+
pass
|
|
162
|
+
for close in (lambda: sess.proc.terminate(), lambda: sess.proc.stdout.close()):
|
|
163
|
+
try:
|
|
164
|
+
close()
|
|
165
|
+
except Exception: # noqa: BLE001
|
|
166
|
+
pass
|
|
167
|
+
self._sessions.pop(rider_id, None)
|
|
168
|
+
|
|
169
|
+
def stop_all(self) -> None:
|
|
170
|
+
for rid in list(self._sessions):
|
|
171
|
+
self.stop(rid)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Housekeeping for the per-element shadow folders under ``~/.gini/xv6-shadows/``.
|
|
2
|
+
|
|
3
|
+
The Load loop bind-mounts one folder per xv6 element (``~/.gini/xv6-shadows/<name>/gini_sched.c``)
|
|
4
|
+
and, by design, never deletes it — a student's shadow persists across Stop/Run. The flip side is
|
|
5
|
+
they accumulate (a renamed/removed element leaves an orphan). This module provides the explicit
|
|
6
|
+
housekeeping: **reset** one element's folder (so the next Run re-seeds the stub) and **prune**
|
|
7
|
+
orphans. Nothing here runs automatically — deleting student work is always a deliberate act.
|
|
8
|
+
|
|
9
|
+
Pure (os/shutil/pathlib only); unit-tested with a ``GINI_HOME_DIR`` override.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _home() -> Path:
|
|
19
|
+
# Same rule as app.paths.gini_home / content.py, replicated to avoid an app import.
|
|
20
|
+
return Path(os.environ.get("GINI_HOME_DIR") or (Path.home() / ".gini")).expanduser()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def shadows_root() -> Path:
|
|
24
|
+
return _home() / "xv6-shadows"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def list_shadows() -> list[str]:
|
|
28
|
+
"""Element names that currently have a shadow folder."""
|
|
29
|
+
root = shadows_root()
|
|
30
|
+
return sorted(p.name for p in root.iterdir() if p.is_dir()) if root.is_dir() else []
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def reset_shadow(name: str) -> bool:
|
|
34
|
+
"""Delete one element's shadow folder so the next Run re-seeds a fresh stub. Returns True if a
|
|
35
|
+
folder was removed. (Live, the Machine Lab's Revert restores the stub *contents* without a Stop;
|
|
36
|
+
this is the offline / start-over path.)"""
|
|
37
|
+
d = shadows_root() / name
|
|
38
|
+
if d.is_dir():
|
|
39
|
+
shutil.rmtree(d, ignore_errors=True)
|
|
40
|
+
return True
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def prune_shadows(keep) -> list[str]:
|
|
45
|
+
"""Remove shadow folders whose element name is NOT in ``keep`` (orphans from deleted/renamed
|
|
46
|
+
elements). Pass the CURRENT set of xv6 element names. Returns the names removed."""
|
|
47
|
+
keep = set(keep or ())
|
|
48
|
+
removed = []
|
|
49
|
+
for name in list_shadows():
|
|
50
|
+
if name not in keep and reset_shadow(name):
|
|
51
|
+
removed.append(name)
|
|
52
|
+
return removed
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Open a platform terminal running a command — the 'double-click to log in' plumbing.
|
|
2
|
+
|
|
3
|
+
Mirrors old gBuilder's xterm-on-double-click, but cross-platform: a real terminal
|
|
4
|
+
window the student types into (docker exec into a machine, or a console into a
|
|
5
|
+
network element).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def open_terminal(title: str, cwd: str | Path, command: str) -> tuple[bool, str]:
|
|
16
|
+
cwd = str(cwd)
|
|
17
|
+
if sys.platform == "darwin":
|
|
18
|
+
script = f'cd {_q(cwd)} && clear && echo {_q("== " + title + " ==")} && {command}'
|
|
19
|
+
osa = f'tell application "Terminal" to do script "{script.replace(chr(34), chr(92) + chr(34))}"'
|
|
20
|
+
try:
|
|
21
|
+
subprocess.Popen(["osascript", "-e", osa,
|
|
22
|
+
"-e", 'tell application "Terminal" to activate'])
|
|
23
|
+
return True, "opened Terminal"
|
|
24
|
+
except FileNotFoundError:
|
|
25
|
+
return False, "osascript not found"
|
|
26
|
+
# Linux: try the common terminal emulators
|
|
27
|
+
for term, args in (
|
|
28
|
+
("x-terminal-emulator", ["-e"]),
|
|
29
|
+
("gnome-terminal", ["--"]),
|
|
30
|
+
("konsole", ["-e"]),
|
|
31
|
+
("xterm", ["-e"]),
|
|
32
|
+
):
|
|
33
|
+
if shutil.which(term):
|
|
34
|
+
full = f"cd {_q(cwd)}; {command}; exec ${{SHELL:-sh}}"
|
|
35
|
+
try:
|
|
36
|
+
subprocess.Popen([term, *args, "bash", "-lc", full])
|
|
37
|
+
return True, f"opened {term}"
|
|
38
|
+
except OSError:
|
|
39
|
+
continue
|
|
40
|
+
return False, ("no terminal emulator found — run manually:\n"
|
|
41
|
+
f" cd {cwd} && {command}")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _q(s: str) -> str:
|
|
45
|
+
return "'" + s.replace("'", "'\\''") + "'"
|
gini/setup/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""GINI setup — the `gini-setup` command that brings in the container runtime + images.
|
|
2
|
+
|
|
3
|
+
`pip install gini-toolkit` gives the desktop app; the heavy runtime (Docker/Colima) and the custom
|
|
4
|
+
container images live outside the wheel. `gini-setup` detects/guides the runtime install and pulls
|
|
5
|
+
the images from the registry, then writes a marker (~/.gini/setup.json) so `gbuilder` knows live Run
|
|
6
|
+
is ready (Demo mode always works without it).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
# Registry namespace for the published images. Placeholder until the GHCR org is created + CI pushes;
|
|
11
|
+
# override with the GINI_REGISTRY env var.
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
REGISTRY = os.environ.get("GINI_REGISTRY", "ghcr.io/gini-toolkit")
|
|
15
|
+
|
|
16
|
+
# The custom images the app needs (third-party images like alpine/postgres are pulled by Compose).
|
|
17
|
+
IMAGES = ["gini-xv6", "gini-oszoo", "gini-grouter", "gini-pox"]
|
gini/setup/cli.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""`gini-setup` — one-time bootstrap of the container runtime + images.
|
|
2
|
+
|
|
3
|
+
gini-setup # detect/guide the runtime, then pull the images
|
|
4
|
+
gini-setup --check # report status and exit
|
|
5
|
+
gini-setup --update # re-pull images (after `pip install -U gini-toolkit`)
|
|
6
|
+
gini-setup --yes # run auto-install steps without prompting
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
from . import images, marker, runtime
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _confirm(question: str) -> bool:
|
|
17
|
+
try:
|
|
18
|
+
return input(f"{question} [y/N] ").strip().lower() in ("y", "yes")
|
|
19
|
+
except EOFError:
|
|
20
|
+
return False
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _app_version() -> str:
|
|
24
|
+
from .. import __version__
|
|
25
|
+
return __version__
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _do_check(os_name: str) -> int:
|
|
29
|
+
print(f"gini-toolkit {_app_version()} · {os_name}")
|
|
30
|
+
print(" runtime:", "available" if runtime.docker_available() else "NOT found")
|
|
31
|
+
if marker.is_setup_done():
|
|
32
|
+
print(f" setup: done (version {marker.setup_version()})")
|
|
33
|
+
if marker.needs_update(_app_version()):
|
|
34
|
+
print(" note: app was upgraded — run `gini-setup --update` to refresh images.")
|
|
35
|
+
else:
|
|
36
|
+
print(" setup: not run yet")
|
|
37
|
+
return 0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _ensure_runtime(os_name: str, assume_yes: bool) -> bool:
|
|
41
|
+
if runtime.docker_available():
|
|
42
|
+
return True
|
|
43
|
+
plan = runtime.runtime_plan(os_name)
|
|
44
|
+
print(f"No container runtime found. GINI uses {plan['runtime']} on {os_name}.")
|
|
45
|
+
if plan["auto"]:
|
|
46
|
+
print("These commands can set it up:")
|
|
47
|
+
for c in plan["auto"]:
|
|
48
|
+
print(" ", c)
|
|
49
|
+
if assume_yes or _confirm("Run them now?"):
|
|
50
|
+
for c in plan["auto"]:
|
|
51
|
+
if runtime.run_shell(c) != 0:
|
|
52
|
+
print("Step failed:", c, "\n\n", plan["manual"])
|
|
53
|
+
return False
|
|
54
|
+
else:
|
|
55
|
+
print("\n" + plan["manual"])
|
|
56
|
+
return False
|
|
57
|
+
else:
|
|
58
|
+
print("\n" + plan["manual"])
|
|
59
|
+
return False
|
|
60
|
+
if not runtime.docker_available():
|
|
61
|
+
print("Runtime installed but not reachable yet — start it and re-run `gini-setup`.")
|
|
62
|
+
return False
|
|
63
|
+
return True
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _pull(os_name: str) -> int:
|
|
67
|
+
refs = images.image_refs(_app_version())
|
|
68
|
+
print("Pulling images:")
|
|
69
|
+
for r in refs:
|
|
70
|
+
print(" ", r)
|
|
71
|
+
results = images.pull_images(refs)
|
|
72
|
+
ok = [r for r, s in results if s]
|
|
73
|
+
bad = [r for r, s in results if not s]
|
|
74
|
+
for r, s in results:
|
|
75
|
+
print(" ok " if s else " FAIL", r)
|
|
76
|
+
marker.write_marker({"version": _app_version(), "os": os_name,
|
|
77
|
+
"tag": images.image_tag(_app_version()), "images": ok})
|
|
78
|
+
print("\nRecorded", marker.marker_path())
|
|
79
|
+
if bad:
|
|
80
|
+
print("\nSome images could not be pulled — they may not be published yet, or the registry "
|
|
81
|
+
"is unreachable. Live Run will be limited until they're available.")
|
|
82
|
+
return 2
|
|
83
|
+
print("\nSetup complete. Launch the app with: gbuilder")
|
|
84
|
+
return 0
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def main(argv=None) -> int:
|
|
88
|
+
ap = argparse.ArgumentParser(prog="gini-setup",
|
|
89
|
+
description="Bring in the GINI container runtime + images.")
|
|
90
|
+
ap.add_argument("--check", action="store_true", help="report status and exit")
|
|
91
|
+
ap.add_argument("--update", action="store_true", help="re-pull images for the current version")
|
|
92
|
+
ap.add_argument("--yes", "-y", action="store_true", help="run auto-install steps without asking")
|
|
93
|
+
args = ap.parse_args(argv)
|
|
94
|
+
os_name = runtime.detect_os()
|
|
95
|
+
|
|
96
|
+
if args.check:
|
|
97
|
+
return _do_check(os_name)
|
|
98
|
+
if args.update:
|
|
99
|
+
if not runtime.docker_available():
|
|
100
|
+
print("No runtime — run `gini-setup` first.")
|
|
101
|
+
return 1
|
|
102
|
+
return _pull(os_name)
|
|
103
|
+
if not _ensure_runtime(os_name, args.yes):
|
|
104
|
+
return 1
|
|
105
|
+
return _pull(os_name)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
sys.exit(main())
|
gini/setup/images.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Pull the custom GINI images from the registry, pinned to the app version."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
from . import IMAGES, REGISTRY
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def image_tag(version: str | None) -> str:
|
|
10
|
+
"""A released version (e.g. 6.1.0) pins images to that tag; a dev/unreleased build uses 'latest'
|
|
11
|
+
(there's no matching published tag yet)."""
|
|
12
|
+
if not version:
|
|
13
|
+
return "latest"
|
|
14
|
+
if "dev" in version or "+" in version or version.startswith("0"):
|
|
15
|
+
return "latest"
|
|
16
|
+
return version
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def image_refs(version: str | None) -> list[str]:
|
|
20
|
+
tag = image_tag(version)
|
|
21
|
+
return [f"{REGISTRY}/{name}:{tag}" for name in IMAGES]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def pull_images(refs, run=subprocess.run) -> list[tuple[str, bool]]:
|
|
25
|
+
"""Pull each ref; return [(ref, ok)]. A failure is captured, not raised (image may be unpublished)."""
|
|
26
|
+
out = []
|
|
27
|
+
for ref in refs:
|
|
28
|
+
try:
|
|
29
|
+
r = run(["docker", "pull", ref], timeout=1800)
|
|
30
|
+
out.append((ref, r.returncode == 0))
|
|
31
|
+
except Exception:
|
|
32
|
+
out.append((ref, False))
|
|
33
|
+
return out
|
gini/setup/marker.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""The setup marker — ~/.gini/setup.json — records that the runtime + images were brought in, so
|
|
2
|
+
`gbuilder` can tell whether live Run is ready without re-probing everything on launch."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def gini_home() -> Path:
|
|
11
|
+
return Path(os.environ.get("GINI_HOME", str(Path.home() / ".gini")))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def marker_path() -> Path:
|
|
15
|
+
return gini_home() / "setup.json"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def read_marker() -> dict:
|
|
19
|
+
try:
|
|
20
|
+
return json.loads(marker_path().read_text())
|
|
21
|
+
except Exception:
|
|
22
|
+
return {}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def write_marker(info: dict) -> None:
|
|
26
|
+
p = marker_path()
|
|
27
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
p.write_text(json.dumps(info, indent=2))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def is_setup_done() -> bool:
|
|
32
|
+
"""True once at least one image has been pulled and recorded."""
|
|
33
|
+
return bool(read_marker().get("images"))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def setup_version() -> str | None:
|
|
37
|
+
return read_marker().get("version")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def needs_update(app_version: str) -> bool:
|
|
41
|
+
"""The app was upgraded past the version setup last ran for -> user should re-run gini-setup."""
|
|
42
|
+
v = setup_version()
|
|
43
|
+
return is_setup_done() and v is not None and v != app_version
|
gini/setup/runtime.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Container-runtime detection + per-OS install guidance.
|
|
2
|
+
|
|
3
|
+
The runtime can't be bundled, and it differs per OS (Colima is macOS/Linux only; Windows uses Docker
|
|
4
|
+
Desktop/Podman). We detect a working Docker socket and, where we can, offer the auto-install commands
|
|
5
|
+
— but only after the user consents (we never silently run privileged installers)."""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import platform
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def detect_os() -> str:
|
|
14
|
+
return {"Darwin": "macos", "Linux": "linux", "Windows": "windows"}.get(
|
|
15
|
+
platform.system(), "unknown")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def docker_available(run=subprocess.run) -> bool:
|
|
19
|
+
"""True if a Docker-compatible CLI is present AND a daemon answers (Colima/Desktop/Engine/Podman)."""
|
|
20
|
+
if not shutil.which("docker"):
|
|
21
|
+
return False
|
|
22
|
+
try:
|
|
23
|
+
r = run(["docker", "info"], capture_output=True, timeout=15)
|
|
24
|
+
return r.returncode == 0
|
|
25
|
+
except Exception:
|
|
26
|
+
return False
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Per-OS plan: the runtime we recommend, the commands we CAN auto-run (with consent), and the manual
|
|
30
|
+
# fallback text. Colima is macOS/Linux; Windows has no Colima.
|
|
31
|
+
_PLANS = {
|
|
32
|
+
"macos": {
|
|
33
|
+
"runtime": "Colima + docker CLI",
|
|
34
|
+
"auto": ["brew install colima docker",
|
|
35
|
+
"colima start --cpu 2 --memory 4 --disk 30"],
|
|
36
|
+
"needs": "Homebrew",
|
|
37
|
+
"manual": ("Install Homebrew from https://brew.sh, then run:\n"
|
|
38
|
+
" brew install colima docker\n"
|
|
39
|
+
" colima start --cpu 2 --memory 4 --disk 30"),
|
|
40
|
+
},
|
|
41
|
+
"linux": {
|
|
42
|
+
"runtime": "Docker Engine",
|
|
43
|
+
"auto": [], # distro-specific + needs sudo -> we guide rather than run
|
|
44
|
+
"needs": "sudo / your package manager",
|
|
45
|
+
"manual": ("Install Docker Engine for your distro "
|
|
46
|
+
"(https://docs.docker.com/engine/install/) and add your user to the 'docker' "
|
|
47
|
+
"group: sudo usermod -aG docker $USER (then log out/in). Podman also works."),
|
|
48
|
+
},
|
|
49
|
+
"windows": {
|
|
50
|
+
"runtime": "Docker Desktop (or Podman Desktop)",
|
|
51
|
+
"auto": ["winget install -e --id Docker.DockerDesktop"],
|
|
52
|
+
"needs": "winget + admin",
|
|
53
|
+
"manual": ("Install Docker Desktop (https://www.docker.com/products/docker-desktop) or "
|
|
54
|
+
"Podman Desktop, then start it. (Colima is not available on Windows.)"),
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def runtime_plan(os_name: str) -> dict:
|
|
60
|
+
return _PLANS.get(os_name, {"runtime": "a Docker-compatible runtime", "auto": [], "needs": "",
|
|
61
|
+
"manual": "Install Docker or a compatible runtime and start it."})
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def run_shell(cmd: str, run=subprocess.run) -> int:
|
|
65
|
+
"""Run one auto-install command, streaming to the console. Returns the exit code."""
|
|
66
|
+
try:
|
|
67
|
+
return run(cmd, shell=True).returncode
|
|
68
|
+
except Exception:
|
|
69
|
+
return 1
|
gini/ui/__init__.py
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
test
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|