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/runtime/switch.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""User-space learning switch.
|
|
2
|
+
|
|
3
|
+
One UDP port per link. Learns source MAC -> ingress port; floods broadcast/multicast
|
|
4
|
+
and unknown unicast to all other ports; unicasts known destinations. This is the
|
|
5
|
+
"multiplexed fabric switch" from the plan, here as a standalone process for the spike.
|
|
6
|
+
|
|
7
|
+
Run as a process: SWITCH_CONFIG='{"name":"s1","ports":[{...}]}' python -m dataplane.switch
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
from .control import maybe_start
|
|
16
|
+
from .frame import is_multicast_mac, parse_eth
|
|
17
|
+
from .transport import Port, run_loop
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LearningSwitch:
|
|
21
|
+
def __init__(self, cfg: dict) -> None:
|
|
22
|
+
self.name = cfg["name"]
|
|
23
|
+
self.ports = [Port.from_cfg(p, name=f"p{i}") for i, p in enumerate(cfg["ports"])]
|
|
24
|
+
self.table: dict[str, Port] = {} # mac -> port
|
|
25
|
+
self.log = cfg.get("log", False)
|
|
26
|
+
self._ctrl = maybe_start(self.name, self._control, f"switch {self.name}")
|
|
27
|
+
|
|
28
|
+
def _control(self, cmd: str) -> str:
|
|
29
|
+
cmd = cmd.lower()
|
|
30
|
+
if cmd in ("help", "?", "h"):
|
|
31
|
+
return "commands: mactable, ports, help, exit"
|
|
32
|
+
if cmd in ("mactable", "mac", "table"):
|
|
33
|
+
if not self.table:
|
|
34
|
+
return "(mac table empty)"
|
|
35
|
+
return "\n".join(f" {m} -> {p.name}" for m, p in self.table.items())
|
|
36
|
+
if cmd == "ports":
|
|
37
|
+
return ", ".join(p.name for p in self.ports)
|
|
38
|
+
return f"unknown command: {cmd} (try 'help')"
|
|
39
|
+
|
|
40
|
+
def handle(self, inport: Port, frame: bytes) -> None:
|
|
41
|
+
dst, src, _etype, _payload = parse_eth(frame)
|
|
42
|
+
self.table[src] = inport
|
|
43
|
+
if dst == "ff:ff:ff:ff:ff:ff" or is_multicast_mac(dst):
|
|
44
|
+
self._flood(inport, frame)
|
|
45
|
+
return
|
|
46
|
+
out = self.table.get(dst)
|
|
47
|
+
if out is None:
|
|
48
|
+
self._flood(inport, frame)
|
|
49
|
+
elif out is not inport:
|
|
50
|
+
out.send(frame)
|
|
51
|
+
if self.log:
|
|
52
|
+
print(f"[{self.name}] {src} -> {dst} in={inport.name}", file=sys.stderr)
|
|
53
|
+
|
|
54
|
+
def _flood(self, inport: Port, frame: bytes) -> None:
|
|
55
|
+
for p in self.ports:
|
|
56
|
+
if p is not inport:
|
|
57
|
+
p.send(frame)
|
|
58
|
+
|
|
59
|
+
def run(self) -> None:
|
|
60
|
+
print(f"[{self.name}] learning switch up, {len(self.ports)} ports", file=sys.stderr)
|
|
61
|
+
run_loop(self.ports, self.handle)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Hub(LearningSwitch):
|
|
65
|
+
"""A Layer-1 repeater — the device a switch replaced.
|
|
66
|
+
|
|
67
|
+
A hub is "dumb wire with more ports": every frame that arrives is repeated out of
|
|
68
|
+
every *other* port, with no MAC learning and no filtering. All ports therefore share
|
|
69
|
+
one collision domain and one broadcast domain. Pedagogically this is the foil to the
|
|
70
|
+
Switch: send a unicast h1->h2 through a hub and h3 still sees it (repeated to all);
|
|
71
|
+
through a switch, once it has learned, only h2 does. The control console makes the
|
|
72
|
+
difference explicit — a hub has no MAC table to show."""
|
|
73
|
+
|
|
74
|
+
def handle(self, inport: Port, frame: bytes) -> None:
|
|
75
|
+
self._flood(inport, frame) # repeat everything; never learn, never filter
|
|
76
|
+
if self.log:
|
|
77
|
+
dst, src, _etype, _payload = parse_eth(frame)
|
|
78
|
+
print(f"[{self.name}] repeat {src} -> {dst} in={inport.name} (flood all)",
|
|
79
|
+
file=sys.stderr)
|
|
80
|
+
|
|
81
|
+
def _control(self, cmd: str) -> str:
|
|
82
|
+
cmd = cmd.lower()
|
|
83
|
+
if cmd in ("help", "?", "h"):
|
|
84
|
+
return ("commands: ports, help, exit "
|
|
85
|
+
"(a hub has no MAC table — it repeats every frame to all ports)")
|
|
86
|
+
if cmd in ("mactable", "mac", "table"):
|
|
87
|
+
return "(a hub has no MAC table — it repeats every frame out all other ports)"
|
|
88
|
+
if cmd == "ports":
|
|
89
|
+
return ", ".join(p.name for p in self.ports)
|
|
90
|
+
return f"unknown command: {cmd} (try 'help')"
|
|
91
|
+
|
|
92
|
+
def run(self) -> None:
|
|
93
|
+
print(f"[{self.name}] hub up (L1 repeater), {len(self.ports)} ports", file=sys.stderr)
|
|
94
|
+
run_loop(self.ports, self.handle)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def make_switch(cfg: dict):
|
|
98
|
+
"""Build the right L2 node for a fabric entry: a Hub (flood-all repeater) when the
|
|
99
|
+
compiler marked it `hub`, else a learning Switch."""
|
|
100
|
+
return Hub(cfg) if cfg.get("hub") else LearningSwitch(cfg)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def main() -> None:
|
|
104
|
+
cfg = json.loads(os.environ["SWITCH_CONFIG"])
|
|
105
|
+
make_switch(cfg).run()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if __name__ == "__main__":
|
|
109
|
+
main()
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Ethernet-in-UDP transport: a Port is a UDP socket carrying one frame per datagram.
|
|
2
|
+
|
|
3
|
+
Same abstraction works two ways:
|
|
4
|
+
* Docker: bind 0.0.0.0:P, peer_host = a service name (Docker DNS), peer_port = P.
|
|
5
|
+
* Loopback test: bind 127.0.0.1 on distinct ports per endpoint.
|
|
6
|
+
|
|
7
|
+
A "port" on a switch/router/host is exactly this. Internal fabric links would later
|
|
8
|
+
swap this for an in-memory ring; the node code wouldn't change.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import selectors
|
|
13
|
+
import socket
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Port:
|
|
17
|
+
def __init__(self, bind_port: int, peer_host: str, peer_port: int,
|
|
18
|
+
bind_host: str = "0.0.0.0", name: str = "") -> None:
|
|
19
|
+
self.name = name
|
|
20
|
+
self.peer_host = peer_host
|
|
21
|
+
self.peer_port = peer_port
|
|
22
|
+
self._peer_ip: str | None = None
|
|
23
|
+
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
24
|
+
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
25
|
+
self.sock.bind((bind_host, bind_port))
|
|
26
|
+
self.sock.setblocking(False)
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def from_cfg(cls, cfg: dict, name: str = "") -> "Port":
|
|
30
|
+
return cls(cfg["bind_port"], cfg["peer_host"], cfg["peer_port"],
|
|
31
|
+
cfg.get("bind_host", "0.0.0.0"), name)
|
|
32
|
+
|
|
33
|
+
def _resolve(self) -> str | None:
|
|
34
|
+
if self._peer_ip is None:
|
|
35
|
+
try:
|
|
36
|
+
self._peer_ip = socket.gethostbyname(self.peer_host)
|
|
37
|
+
except OSError:
|
|
38
|
+
return None # DNS not ready yet (Docker startup) — caller drops
|
|
39
|
+
return self._peer_ip
|
|
40
|
+
|
|
41
|
+
def send(self, frame: bytes) -> None:
|
|
42
|
+
ip = self._resolve()
|
|
43
|
+
if ip is None:
|
|
44
|
+
return
|
|
45
|
+
try:
|
|
46
|
+
self.sock.sendto(frame, (ip, self.peer_port))
|
|
47
|
+
except OSError:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
def recv(self) -> bytes | None:
|
|
51
|
+
try:
|
|
52
|
+
data, _ = self.sock.recvfrom(65535)
|
|
53
|
+
return data
|
|
54
|
+
except BlockingIOError:
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def run_loop(ports: list[Port], handler, tick=None, tick_interval: float = 0.5) -> None:
|
|
59
|
+
"""Select over all ports; call handler(port, frame) for each frame.
|
|
60
|
+
|
|
61
|
+
handler receives the Port the frame arrived on, so a node can identify its
|
|
62
|
+
ingress port without any per-frame addressing.
|
|
63
|
+
"""
|
|
64
|
+
sel = selectors.DefaultSelector()
|
|
65
|
+
for p in ports:
|
|
66
|
+
sel.register(p.sock, selectors.EVENT_READ, p)
|
|
67
|
+
while True:
|
|
68
|
+
events = sel.select(timeout=tick_interval)
|
|
69
|
+
for key, _ in events:
|
|
70
|
+
port: Port = key.data
|
|
71
|
+
while True:
|
|
72
|
+
frame = port.recv()
|
|
73
|
+
if frame is None:
|
|
74
|
+
break
|
|
75
|
+
handler(port, frame)
|
|
76
|
+
if tick is not None:
|
|
77
|
+
tick()
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""xv6 live bridge (Mac side) — fills MachineState from the in-container agent over HTTP.
|
|
2
|
+
|
|
3
|
+
The gdb reads happen INSIDE the gini-xv6 container (backend/xv6/gini_agent.py), where gdb and
|
|
4
|
+
the kernel symbols live; this bridge just makes plain HTTP calls to the agent's published port
|
|
5
|
+
and parses the returned text with the already-tested domain parsers. It is a drop-in for the
|
|
6
|
+
offline DemoScheduler: snapshot()/step()/set_timeslice()/set_policy() plus `.vm`/`.fs` readers,
|
|
7
|
+
so MachineState, the Machine Lab, and the Ask GINI card use it unchanged.
|
|
8
|
+
|
|
9
|
+
The HTTP calls are injectable (`get=`/`post=`) so the compose + parsing logic is unit-tested
|
|
10
|
+
without a running container; the VM/FS readers fall back to the demo shape on any error, so the
|
|
11
|
+
Memory/Storage dialogs always open even if the live read momentarily fails.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import urllib.request
|
|
17
|
+
|
|
18
|
+
from ..domain.xv6 import (
|
|
19
|
+
Snapshot, apply_proc_sched, parse_backtrace, parse_cpu_lines, parse_cpu_regs, parse_csr,
|
|
20
|
+
parse_modetime, parse_policies, parse_procdump, parse_registers, parse_regs_line, parse_sched,
|
|
21
|
+
parse_shadow_manifest, running_pid,
|
|
22
|
+
)
|
|
23
|
+
from ..domain.xv6_fs import FsSnapshot, Superblock, layout, parse_logheader, parse_superblock
|
|
24
|
+
from ..domain.xv6_vm import VmSnapshot, parse_faults, parse_vmall, parse_vmprint
|
|
25
|
+
|
|
26
|
+
# must match gini_pick() in gini_patch.py: 0=round-robin 1=priority 2=lottery. Custom student
|
|
27
|
+
# policies (MLFQ, stride, …) get their own ids when added via the Scheduler Builder.
|
|
28
|
+
POLICY_ID = {"round-robin": 0, "priority": 1, "lottery": 2}
|
|
29
|
+
# each shadow overrides the policy at this index (the kernel toggles the CURRENT policy's shadow).
|
|
30
|
+
SHADOW_POLICY = {"rr_sched": 0, "prio_sched": 1, "lottery_sched": 2}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AgentClient:
|
|
34
|
+
"""HTTP client for the in-container agent. `get`/`post` are injectable for tests."""
|
|
35
|
+
|
|
36
|
+
# longer than the agent's own 6s gdb timeout, so a slow proc-walk under CPU load returns a
|
|
37
|
+
# result (even an empty one we can ignore) instead of the client abandoning it mid-read.
|
|
38
|
+
def __init__(self, base_url: str, get=None, post=None, timeout: float = 10.0) -> None:
|
|
39
|
+
self.base = base_url.rstrip("/")
|
|
40
|
+
self.timeout = timeout
|
|
41
|
+
self._get = get or self._http_get
|
|
42
|
+
self._post = post or self._http_post
|
|
43
|
+
|
|
44
|
+
def _http_get(self, url: str) -> str: # pragma: no cover - real network
|
|
45
|
+
with urllib.request.urlopen(url, timeout=self.timeout) as r:
|
|
46
|
+
return r.read().decode(errors="replace")
|
|
47
|
+
|
|
48
|
+
def _http_post(self, url: str, body: str = "") -> str: # pragma: no cover - real network
|
|
49
|
+
req = urllib.request.Request(url, data=body.encode() if body else None, method="POST")
|
|
50
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as r:
|
|
51
|
+
return r.read().decode(errors="replace")
|
|
52
|
+
|
|
53
|
+
def get_json(self, path: str) -> dict:
|
|
54
|
+
try:
|
|
55
|
+
return json.loads(self._get(self.base + path))
|
|
56
|
+
except Exception:
|
|
57
|
+
return {}
|
|
58
|
+
|
|
59
|
+
def get_text(self, path: str) -> str:
|
|
60
|
+
try:
|
|
61
|
+
return self._get(self.base + path)
|
|
62
|
+
except Exception:
|
|
63
|
+
return ""
|
|
64
|
+
|
|
65
|
+
def post(self, path: str, body: str = "") -> str:
|
|
66
|
+
try:
|
|
67
|
+
return self._post(self.base + path, body) if body else self._post(self.base + path)
|
|
68
|
+
except TypeError:
|
|
69
|
+
return self._post(self.base + path) # injected posts without a body arg
|
|
70
|
+
except Exception:
|
|
71
|
+
return ""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class _VmReader:
|
|
75
|
+
def __init__(self, agent: AgentClient) -> None:
|
|
76
|
+
self.agent = agent
|
|
77
|
+
|
|
78
|
+
def snapshot(self):
|
|
79
|
+
# REAL only: return the live page table, or an explicit no-data VmSnapshot. Never fake.
|
|
80
|
+
# The region map + physical-allocator bar aren't dumped for real yet, so they're left
|
|
81
|
+
# empty (not in `have`) rather than borrowed from the demo.
|
|
82
|
+
try:
|
|
83
|
+
vm = parse_vmprint(self.agent.get_text("/vm"))
|
|
84
|
+
if vm.leaves: # got real mappings
|
|
85
|
+
vm.source, vm.ok, vm.have = "real", True, ("pagetable",)
|
|
86
|
+
return vm
|
|
87
|
+
except Exception:
|
|
88
|
+
pass
|
|
89
|
+
return VmSnapshot(source="real", ok=False, have=())
|
|
90
|
+
|
|
91
|
+
def all_procs(self):
|
|
92
|
+
"""Every user process's page table (gini_vmdump_all) -> {pid: ProcVm}, for the two-proc
|
|
93
|
+
COW / sharing view. REAL only: empty dict if the read fails (the face shows an error)."""
|
|
94
|
+
try:
|
|
95
|
+
procs = parse_vmall(self.agent.get_text("/vmall"))
|
|
96
|
+
if procs:
|
|
97
|
+
return procs
|
|
98
|
+
except Exception:
|
|
99
|
+
pass
|
|
100
|
+
return {}
|
|
101
|
+
|
|
102
|
+
def faults(self):
|
|
103
|
+
"""The live page-fault ring (gini_faultdump) -> [PageFault]. REAL only: empty list if the
|
|
104
|
+
read fails. Classification happens in the Memory face against the all-procs picture."""
|
|
105
|
+
try:
|
|
106
|
+
f = parse_faults(self.agent.get_text("/faults"))
|
|
107
|
+
if f:
|
|
108
|
+
return f
|
|
109
|
+
except Exception:
|
|
110
|
+
pass
|
|
111
|
+
return []
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class _FsReader:
|
|
115
|
+
def __init__(self, agent: AgentClient) -> None:
|
|
116
|
+
self.agent = agent
|
|
117
|
+
|
|
118
|
+
def snapshot(self):
|
|
119
|
+
# REAL only: superblock + log are dumped for real today; inodes/dir/bcache are NOT, so
|
|
120
|
+
# they stay empty and out of `have` (the face marks them "not available", never fakes).
|
|
121
|
+
# A failed read returns ok=False so the face shows an error instead of demo data.
|
|
122
|
+
try:
|
|
123
|
+
txt = self.agent.get_text("/fs") # gini_fsdump over the serial (no gdb)
|
|
124
|
+
sb = parse_superblock(txt)
|
|
125
|
+
if sb.size:
|
|
126
|
+
log = parse_logheader(txt, start=sb.logstart, size=sb.nlog)
|
|
127
|
+
return FsSnapshot(sb=sb, regions=layout(sb), log=log,
|
|
128
|
+
source="real", ok=True, have=("layout", "log"))
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
return FsSnapshot(sb=Superblock(), source="real", ok=False, have=())
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class Xv6Bridge:
|
|
135
|
+
"""The live provider (talks HTTP to the in-container agent)."""
|
|
136
|
+
|
|
137
|
+
def __init__(self, agent: AgentClient, quantum: int = 1) -> None:
|
|
138
|
+
self.agent = agent
|
|
139
|
+
self.timeslice = int(quantum)
|
|
140
|
+
self.policy = "round-robin"
|
|
141
|
+
self.vm = _VmReader(agent)
|
|
142
|
+
self.fs = _FsReader(agent)
|
|
143
|
+
self._seq = 0
|
|
144
|
+
self._last_cpu = None # register/stack detail (from gdb) — reused between fast reads
|
|
145
|
+
self._last_stack: list = []
|
|
146
|
+
self.kernel_quantum = None # the kernel's ACTUAL sched_quantum (from the SCHED line)
|
|
147
|
+
self.kernel_policy = None # the kernel's ACTUAL sched_policy id (from the SCHED line)
|
|
148
|
+
self.kernel_policies = {} # {id: name} roster (POLICY lines) — drives the UI selector
|
|
149
|
+
|
|
150
|
+
def snapshot(self) -> Snapshot:
|
|
151
|
+
"""The Run/observe read: gini_dump() over the serial gives BOTH the process table AND
|
|
152
|
+
the running process's live registers in one shot — FAST and NO halt, so scheduling keeps
|
|
153
|
+
running and registers (sp/satp change per process!) update live. The kernel stack (bt)
|
|
154
|
+
still needs gdb, so it's only refreshed on Step; here we reuse the last one."""
|
|
155
|
+
self._seq += 1
|
|
156
|
+
txt = self.agent.get_text("/procs")
|
|
157
|
+
procs = apply_proc_sched(parse_procdump(txt), txt) # procs + priority/tickets/level/aging
|
|
158
|
+
sched = parse_sched(txt) # the kernel's ACTUAL quantum + policy
|
|
159
|
+
if sched:
|
|
160
|
+
self.kernel_quantum = sched.get("quantum")
|
|
161
|
+
self.kernel_policy = sched.get("policy")
|
|
162
|
+
roster = parse_policies(txt) # {id: name}; drives the UI policy selector
|
|
163
|
+
if roster:
|
|
164
|
+
self.kernel_policies = roster
|
|
165
|
+
cpu = parse_regs_line(txt) # live registers from the same no-halt dump
|
|
166
|
+
if cpu.regs:
|
|
167
|
+
self._last_cpu = cpu
|
|
168
|
+
else:
|
|
169
|
+
cpu = self._last_cpu # CPU idle / no REGS line -> keep last
|
|
170
|
+
return Snapshot(procs=procs, running_pid=running_pid(procs), ticks=self._seq,
|
|
171
|
+
cpu=cpu, stack=self._last_stack, cpus=parse_cpu_lines(txt),
|
|
172
|
+
cpu_regs=parse_cpu_regs(txt),
|
|
173
|
+
modetime=parse_modetime(txt), csr=parse_csr(txt))
|
|
174
|
+
|
|
175
|
+
def _detail_snapshot(self) -> Snapshot:
|
|
176
|
+
"""A full gdb read (halts the guest briefly): registers + kernel stack + a proc walk.
|
|
177
|
+
Used on open and on Step, where a momentary halt is expected."""
|
|
178
|
+
d = self.agent.get_json("/snapshot")
|
|
179
|
+
self._last_cpu = parse_registers(d.get("registers", ""))
|
|
180
|
+
self._last_stack = parse_backtrace(d.get("bt", ""))
|
|
181
|
+
procs = parse_procdump(d.get("procs", ""))
|
|
182
|
+
return Snapshot(procs=procs, running_pid=running_pid(procs), ticks=self._seq,
|
|
183
|
+
cpu=self._last_cpu, stack=self._last_stack)
|
|
184
|
+
|
|
185
|
+
def step(self) -> Snapshot:
|
|
186
|
+
self.agent.post("/step")
|
|
187
|
+
self._seq += 1
|
|
188
|
+
return self._detail_snapshot() # halted at swtch -> full frozen detail
|
|
189
|
+
|
|
190
|
+
def set_timeslice(self, ticks: int) -> None:
|
|
191
|
+
self.timeslice = int(ticks)
|
|
192
|
+
self.agent.post(f"/control?quantum={int(ticks)}")
|
|
193
|
+
|
|
194
|
+
def set_policy(self, policy: str) -> None:
|
|
195
|
+
self.policy = policy
|
|
196
|
+
# map the display name -> id via the live kernel roster (so a NEW policy works), falling
|
|
197
|
+
# back to the built-in ids for the shipped three.
|
|
198
|
+
by_name = {name: pid for pid, name in self.kernel_policies.items()}
|
|
199
|
+
pid = by_name.get(policy, POLICY_ID.get(policy, 0))
|
|
200
|
+
self.agent.post(f"/control?policy={pid}")
|
|
201
|
+
|
|
202
|
+
# -- shadows + control-plane operations (NOT system calls) --------------------- #
|
|
203
|
+
def shadows(self) -> dict:
|
|
204
|
+
"""The shadow manifest -> {name: ShadowStatus}: which student shadows are present, enabled,
|
|
205
|
+
active, and fault-free. The liveness signal the assignment oracle checks first."""
|
|
206
|
+
return parse_shadow_manifest(self.agent.get_text("/shadows"))
|
|
207
|
+
|
|
208
|
+
def set_shadow(self, name: str, on: bool) -> None:
|
|
209
|
+
"""Enable/disable a shadow. The kernel only knows how to toggle the CURRENT policy's shadow,
|
|
210
|
+
so we make that policy active, then toggle iff its state doesn't already match — all the
|
|
211
|
+
naming/targeting logic lives here (testable) and the kernel stays dumb."""
|
|
212
|
+
idx = SHADOW_POLICY.get(name)
|
|
213
|
+
if idx is None:
|
|
214
|
+
return
|
|
215
|
+
self.agent.post(f"/control?policy={idx}") # make that policy active
|
|
216
|
+
cur = self.shadows().get(name)
|
|
217
|
+
if cur is None or bool(cur.enabled) != bool(on):
|
|
218
|
+
self.agent.post("/shadow/toggle") # flip the current policy's shadow
|
|
219
|
+
|
|
220
|
+
def set_priority(self, pid: int, value: int) -> None:
|
|
221
|
+
"""Control-plane op (not a syscall): set a process's scheduling priority so the workload
|
|
222
|
+
has real differences to schedule on."""
|
|
223
|
+
self.agent.post(f"/control/priority?pid={int(pid)}&v={int(value)}")
|
|
224
|
+
|
|
225
|
+
def set_tickets(self, pid: int, n: int) -> None:
|
|
226
|
+
"""Control-plane op (not a syscall): set a process's lottery ticket count."""
|
|
227
|
+
self.agent.post(f"/control/tickets?pid={int(pid)}&n={int(n)}")
|
|
228
|
+
|
|
229
|
+
def load(self) -> tuple[bool, str]:
|
|
230
|
+
"""The Load loop: rebuild the kernel in the container (incremental make) and restart QEMU
|
|
231
|
+
with the new kernel. Returns (ok, log) — log carries compile errors scoped to the student's
|
|
232
|
+
shadow file on failure."""
|
|
233
|
+
try:
|
|
234
|
+
d = json.loads(self.agent.post("/rebuild"))
|
|
235
|
+
return bool(d.get("ok")), str(d.get("log", ""))
|
|
236
|
+
except Exception as e:
|
|
237
|
+
return False, f"load failed: {e}"
|
|
238
|
+
|
|
239
|
+
def revert(self) -> tuple[bool, str]:
|
|
240
|
+
"""Restore the shipped stub shadow and rebuild — one click back to a known-good kernel."""
|
|
241
|
+
try:
|
|
242
|
+
d = json.loads(self.agent.post("/revert"))
|
|
243
|
+
return bool(d.get("ok")), str(d.get("log", ""))
|
|
244
|
+
except Exception as e:
|
|
245
|
+
return False, f"revert failed: {e}"
|
|
246
|
+
|
|
247
|
+
# -- programs (launch / kill / console) via the in-container agent's serial ---- #
|
|
248
|
+
def programs(self) -> list:
|
|
249
|
+
return self.agent.get_json("/programs").get("programs", [])
|
|
250
|
+
|
|
251
|
+
def run(self, prog: str) -> bool:
|
|
252
|
+
try:
|
|
253
|
+
return bool(json.loads(self.agent.post(f"/run?prog={prog}")).get("ok"))
|
|
254
|
+
except Exception:
|
|
255
|
+
return False
|
|
256
|
+
|
|
257
|
+
def kill(self, pid: int) -> None:
|
|
258
|
+
self.agent.post(f"/kill?pid={int(pid)}")
|
|
259
|
+
|
|
260
|
+
def sc(self) -> str:
|
|
261
|
+
"""Raw gini_scdump text (SC counts + TRACE ring) for the histogram + strace panels."""
|
|
262
|
+
return self.agent.get_text("/sc")
|
|
263
|
+
|
|
264
|
+
def traps(self) -> str:
|
|
265
|
+
"""Raw gini_trapdump text (TC per-kind counters + TR trap ring) for the Traps face."""
|
|
266
|
+
return self.agent.get_text("/traps")
|
|
267
|
+
|
|
268
|
+
def catch_trap(self, kind: str = "any"):
|
|
269
|
+
"""Freeze the next live user trap (gdb /trapcatch) and parse it into a TrapFrame — the
|
|
270
|
+
real scause/sepc/stval + saved user registers that seed the CPU journey. `kind` conditions
|
|
271
|
+
the breakpoint (pagefault/syscall/timer/illegal/device). Returns a not-ok TrapFrame on
|
|
272
|
+
timeout/idle, so the journey falls back to its authored captions."""
|
|
273
|
+
from ..domain.xv6 import parse_trapframe
|
|
274
|
+
raw = self.agent.post(f"/trapcatch?kind={kind}")
|
|
275
|
+
txt = ""
|
|
276
|
+
try:
|
|
277
|
+
txt = json.loads(raw).get("out", "")
|
|
278
|
+
except Exception:
|
|
279
|
+
txt = raw or ""
|
|
280
|
+
return parse_trapframe(txt)
|
|
281
|
+
|
|
282
|
+
def alarms(self) -> str:
|
|
283
|
+
"""Raw gini_dump text (contains the per-proc `ALARM …` lines) for the sigalarm-lab strip."""
|
|
284
|
+
return self.agent.get_text("/procs")
|
|
285
|
+
|
|
286
|
+
def console(self) -> str:
|
|
287
|
+
return self.agent.get_text("/console")
|
|
288
|
+
|
|
289
|
+
def console_since(self, since: int = 0) -> tuple[str, int]:
|
|
290
|
+
"""Append-only console delta for the Terminal: (new text after `since`, new cursor). The
|
|
291
|
+
Terminal keeps the cursor and appends, so output streams in instead of re-rendering."""
|
|
292
|
+
d = self.agent.get_json(f"/console/stream?since={int(since)}")
|
|
293
|
+
return d.get("text", ""), int(d.get("next", since))
|
|
294
|
+
|
|
295
|
+
def clear_console(self) -> None:
|
|
296
|
+
"""Blank the Screen: tell the agent to move its console baseline to 'now', so the tail
|
|
297
|
+
starts fresh (the kernel keeps running; this only affects what the console view shows)."""
|
|
298
|
+
self.agent.post("/console/clear")
|
|
299
|
+
|
|
300
|
+
def send_input(self, text: str) -> None:
|
|
301
|
+
self.agent.post("/input", body=text)
|
|
302
|
+
|
|
303
|
+
def interrupt(self) -> None:
|
|
304
|
+
"""Break a hung foreground program (xv6 has no Ctrl-C/SIGINT). Sends the kernel's break
|
|
305
|
+
control-char over the serial; the console driver handles it directly (not sh), so it works
|
|
306
|
+
even while a foreground process has the shell blocked in wait()."""
|
|
307
|
+
self.agent.post("/interrupt")
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def connect(agent_port: int, host: str = "127.0.0.1", quantum: int = 1) -> Xv6Bridge:
|
|
311
|
+
"""Build a live bridge to a running gini-xv6 container's agent (published `agent_port`)."""
|
|
312
|
+
return Xv6Bridge(AgentClient(f"http://{host}:{agent_port}"), quantum=quantum)
|
gini/server/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""GINI server — an authenticated broker in front of Docker/Kata.
|
|
2
|
+
|
|
3
|
+
Students never get a Docker socket or SSH. gBuilder sends a *topology* (the device/link
|
|
4
|
+
model, not a compose file) to this server, which runs on the Kata host; the server compiles
|
|
5
|
+
it with GINI's own trusted compiler, enforces policy (allowed images, Kata/runc runtimes
|
|
6
|
+
only, no privileged or host mounts, per-student resource caps + namespacing), and runs it.
|
|
7
|
+
The constrained API is the only surface students can reach — Docker never sees a
|
|
8
|
+
student-supplied command.
|
|
9
|
+
|
|
10
|
+
Pieces:
|
|
11
|
+
* auth.py — flat user file (PBKDF2 hashes) + HMAC-signed session tokens.
|
|
12
|
+
* policy.py — validate/sanitize a compiled RuntimeConfig before it can run.
|
|
13
|
+
* session.py — per-student project name + workdir (multi-tenant isolation).
|
|
14
|
+
* app.py — the request router (GiniServer.handle) + a stdlib HTTP wrapper.
|
|
15
|
+
"""
|
|
16
|
+
from .app import GiniServer
|
|
17
|
+
from .auth import Tokens, UserStore, hash_password
|
|
18
|
+
from .policy import PolicyError, enforce
|
|
19
|
+
from .session import SessionManager
|
|
20
|
+
|
|
21
|
+
__all__ = ["GiniServer", "UserStore", "Tokens", "hash_password",
|
|
22
|
+
"PolicyError", "enforce", "SessionManager"]
|
gini/server/__main__.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Run the GINI server, or manage its flat user file.
|
|
2
|
+
|
|
3
|
+
python -m gini.server adduser users.json s_jane
|
|
4
|
+
python -m gini.server run --users users.json --base ./gini-labs --port 10000
|
|
5
|
+
|
|
6
|
+
The server secret comes from $GINI_SERVER_SECRET (else a random one each start, which just
|
|
7
|
+
invalidates old tokens on restart — fine for a class).
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import getpass
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _adduser(argv: list[str]) -> int:
|
|
19
|
+
if len(argv) < 2:
|
|
20
|
+
print("usage: adduser <userfile> <username>")
|
|
21
|
+
return 2
|
|
22
|
+
from .auth import hash_password
|
|
23
|
+
path, user = Path(argv[0]), argv[1]
|
|
24
|
+
users = json.loads(path.read_text()) if path.exists() else {}
|
|
25
|
+
users[user] = hash_password(getpass.getpass(f"password for {user}: "))
|
|
26
|
+
path.write_text(json.dumps(users, indent=2))
|
|
27
|
+
print(f"saved {user} to {path}")
|
|
28
|
+
return 0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _run(argv: list[str]) -> int:
|
|
32
|
+
import argparse
|
|
33
|
+
ap = argparse.ArgumentParser(prog="gini.server run")
|
|
34
|
+
ap.add_argument("--users", default="users.json")
|
|
35
|
+
ap.add_argument("--base", default="./gini-labs")
|
|
36
|
+
ap.add_argument("--host", default="0.0.0.0")
|
|
37
|
+
ap.add_argument("--port", type=int, default=10000)
|
|
38
|
+
ap.add_argument("--max-cpus", type=float, default=2.0)
|
|
39
|
+
a = ap.parse_args(argv)
|
|
40
|
+
|
|
41
|
+
from .. import runtime as _rt
|
|
42
|
+
from ..services.orchestrator import Orchestrator
|
|
43
|
+
from .app import GiniServer, serve
|
|
44
|
+
from .auth import Tokens, UserStore
|
|
45
|
+
from .session import SessionManager
|
|
46
|
+
|
|
47
|
+
runtime_dir = Path(_rt.__file__).parent
|
|
48
|
+
secret = os.environ.get("GINI_SERVER_SECRET", os.urandom(32).hex()).encode()
|
|
49
|
+
|
|
50
|
+
def factory(project: str, workdir):
|
|
51
|
+
return Orchestrator(runtime_dir, project=project)
|
|
52
|
+
|
|
53
|
+
srv = GiniServer(UserStore.from_file(a.users), Tokens(secret),
|
|
54
|
+
SessionManager(a.base), factory, max_cpus=a.max_cpus)
|
|
55
|
+
serve(srv, a.host, a.port)
|
|
56
|
+
return 0
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main(argv: list[str] | None = None) -> int:
|
|
60
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
61
|
+
if not argv:
|
|
62
|
+
print("usage: python -m gini.server {run|adduser} ...")
|
|
63
|
+
return 2
|
|
64
|
+
cmd, rest = argv[0], argv[1:]
|
|
65
|
+
if cmd == "adduser":
|
|
66
|
+
return _adduser(rest)
|
|
67
|
+
if cmd == "run":
|
|
68
|
+
return _run(rest)
|
|
69
|
+
print(f"unknown command: {cmd}")
|
|
70
|
+
return 2
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
raise SystemExit(main())
|