crabcode 0.1.6__py3-none-win_amd64.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.
Files changed (205) hide show
  1. crabcode-0.1.6.dist-info/METADATA +63 -0
  2. crabcode-0.1.6.dist-info/RECORD +205 -0
  3. crabcode-0.1.6.dist-info/WHEEL +4 -0
  4. crabcode-0.1.6.dist-info/entry_points.txt +2 -0
  5. crabcode_cli/__init__.py +7 -0
  6. crabcode_cli/__main__.py +4 -0
  7. crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
  8. crabcode_cli/_native/runtime.json +6 -0
  9. crabcode_cli/app.py +776 -0
  10. crabcode_cli/banner.py +39 -0
  11. crabcode_cli/computer_use.py +286 -0
  12. crabcode_cli/model_manager.py +649 -0
  13. crabcode_cli/model_picker.py +168 -0
  14. crabcode_cli/pipe.py +148 -0
  15. crabcode_cli/render/__init__.py +0 -0
  16. crabcode_cli/render/messages.py +19 -0
  17. crabcode_cli/repl.py +4841 -0
  18. crabcode_cli/session_picker.py +239 -0
  19. crabcode_core/__init__.py +7 -0
  20. crabcode_core/_version.py +3 -0
  21. crabcode_core/_windows_process.py +68 -0
  22. crabcode_core/agent_manager.py +1731 -0
  23. crabcode_core/api/__init__.py +20 -0
  24. crabcode_core/api/anthropic_adapter.py +648 -0
  25. crabcode_core/api/azure_adapter.py +69 -0
  26. crabcode_core/api/base.py +233 -0
  27. crabcode_core/api/codex_adapter.py +1327 -0
  28. crabcode_core/api/error_logging.py +132 -0
  29. crabcode_core/api/gemini_adapter.py +26 -0
  30. crabcode_core/api/model_info.py +257 -0
  31. crabcode_core/api/network.py +62 -0
  32. crabcode_core/api/ollama_adapter.py +27 -0
  33. crabcode_core/api/openai_adapter.py +323 -0
  34. crabcode_core/api/registry.py +58 -0
  35. crabcode_core/command_line.py +18 -0
  36. crabcode_core/compact/__init__.py +17 -0
  37. crabcode_core/compact/compact.py +514 -0
  38. crabcode_core/compact/context_tokens.py +165 -0
  39. crabcode_core/computer_use_mode.py +38 -0
  40. crabcode_core/computer_use_observation.py +144 -0
  41. crabcode_core/config/__init__.py +3 -0
  42. crabcode_core/config/claudemd.py +61 -0
  43. crabcode_core/config/manager.py +237 -0
  44. crabcode_core/config/model_catalog.py +256 -0
  45. crabcode_core/events.py +4896 -0
  46. crabcode_core/file_lock.py +81 -0
  47. crabcode_core/filesystem.py +22 -0
  48. crabcode_core/goal.py +149 -0
  49. crabcode_core/hooks/__init__.py +5 -0
  50. crabcode_core/hooks/manager.py +290 -0
  51. crabcode_core/image_io.py +111 -0
  52. crabcode_core/io_worker.py +211 -0
  53. crabcode_core/logging_utils.py +168 -0
  54. crabcode_core/lsp/__init__.py +6 -0
  55. crabcode_core/lsp/client.py +786 -0
  56. crabcode_core/lsp/diagnostics.py +150 -0
  57. crabcode_core/lsp/manager.py +583 -0
  58. crabcode_core/mcp/__init__.py +4 -0
  59. crabcode_core/mcp/client.py +169 -0
  60. crabcode_core/mcp/config.py +57 -0
  61. crabcode_core/path_validation.py +34 -0
  62. crabcode_core/paths.py +24 -0
  63. crabcode_core/peer/__init__.py +10 -0
  64. crabcode_core/peer/runtime.py +518 -0
  65. crabcode_core/permissions/__init__.py +7 -0
  66. crabcode_core/permissions/ai_reviewer.py +184 -0
  67. crabcode_core/permissions/manager.py +187 -0
  68. crabcode_core/plan/__init__.py +5 -0
  69. crabcode_core/plan/executor.py +270 -0
  70. crabcode_core/plan/types.py +169 -0
  71. crabcode_core/prompts/__init__.py +0 -0
  72. crabcode_core/prompts/blocks.py +10 -0
  73. crabcode_core/prompts/context.py +229 -0
  74. crabcode_core/prompts/library.py +92 -0
  75. crabcode_core/prompts/profile.py +79 -0
  76. crabcode_core/prompts/system.py +356 -0
  77. crabcode_core/prompts/templates.py +104 -0
  78. crabcode_core/query/__init__.py +0 -0
  79. crabcode_core/query/computer_use_history.py +83 -0
  80. crabcode_core/query/loop.py +2446 -0
  81. crabcode_core/query/retry.py +116 -0
  82. crabcode_core/schedule/__init__.py +13 -0
  83. crabcode_core/schedule/manager.py +631 -0
  84. crabcode_core/schedule/models.py +204 -0
  85. crabcode_core/schedule/store.py +558 -0
  86. crabcode_core/schedule/timing.py +287 -0
  87. crabcode_core/session/__init__.py +12 -0
  88. crabcode_core/session/export.py +141 -0
  89. crabcode_core/session/meta_db.py +684 -0
  90. crabcode_core/session/storage.py +2032 -0
  91. crabcode_core/session/title_gen.py +52 -0
  92. crabcode_core/skills/__init__.py +6 -0
  93. crabcode_core/skills/context.py +23 -0
  94. crabcode_core/skills/loader.py +214 -0
  95. crabcode_core/skills/matcher.py +203 -0
  96. crabcode_core/snapshot/__init__.py +21 -0
  97. crabcode_core/snapshot/snapshot.py +436 -0
  98. crabcode_core/snapshot/tracker.py +359 -0
  99. crabcode_core/subprocess_utils.py +260 -0
  100. crabcode_core/team/__init__.py +27 -0
  101. crabcode_core/team/bridge.py +66 -0
  102. crabcode_core/team/inbox.py +237 -0
  103. crabcode_core/team/manager.py +1660 -0
  104. crabcode_core/team/message_bus.py +821 -0
  105. crabcode_core/team/models.py +191 -0
  106. crabcode_core/team/recovery.py +192 -0
  107. crabcode_core/text_io.py +52 -0
  108. crabcode_core/tools/__init__.py +112 -0
  109. crabcode_core/tools/_input_helpers.py +39 -0
  110. crabcode_core/tools/_input_schema.py +130 -0
  111. crabcode_core/tools/agent.py +413 -0
  112. crabcode_core/tools/apply_patch.py +587 -0
  113. crabcode_core/tools/ask_user.py +122 -0
  114. crabcode_core/tools/bash.py +154 -0
  115. crabcode_core/tools/browser.py +678 -0
  116. crabcode_core/tools/checklist.py +241 -0
  117. crabcode_core/tools/checkpoint.py +78 -0
  118. crabcode_core/tools/computer_use.py +599 -0
  119. crabcode_core/tools/diff_utils.py +123 -0
  120. crabcode_core/tools/file_edit.py +240 -0
  121. crabcode_core/tools/file_read.py +187 -0
  122. crabcode_core/tools/file_write.py +154 -0
  123. crabcode_core/tools/glob.py +123 -0
  124. crabcode_core/tools/goal.py +134 -0
  125. crabcode_core/tools/grep.py +247 -0
  126. crabcode_core/tools/image.py +173 -0
  127. crabcode_core/tools/image_generate.py +115 -0
  128. crabcode_core/tools/lint.py +388 -0
  129. crabcode_core/tools/loading.py +187 -0
  130. crabcode_core/tools/memory.py +456 -0
  131. crabcode_core/tools/monitor.py +677 -0
  132. crabcode_core/tools/peer.py +103 -0
  133. crabcode_core/tools/revert.py +90 -0
  134. crabcode_core/tools/schedule.py +683 -0
  135. crabcode_core/tools/skill.py +108 -0
  136. crabcode_core/tools/switch_mode.py +213 -0
  137. crabcode_core/tools/team.py +329 -0
  138. crabcode_core/tools/tool_search.py +64 -0
  139. crabcode_core/tools/web_search.py +444 -0
  140. crabcode_core/turn_details.py +64 -0
  141. crabcode_core/types/__init__.py +95 -0
  142. crabcode_core/types/config.py +434 -0
  143. crabcode_core/types/event.py +334 -0
  144. crabcode_core/types/message.py +263 -0
  145. crabcode_core/types/tool.py +208 -0
  146. crabcode_core/usage.py +289 -0
  147. crabcode_core/utf8_sanitize.py +29 -0
  148. crabcode_debugger/__init__.py +5 -0
  149. crabcode_debugger/adapters.py +332 -0
  150. crabcode_debugger/dap.py +304 -0
  151. crabcode_debugger/memory.py +1806 -0
  152. crabcode_debugger/process.py +707 -0
  153. crabcode_debugger/runtime.py +58 -0
  154. crabcode_debugger/sessions.py +387 -0
  155. crabcode_debugger/tool.py +731 -0
  156. crabcode_gateway/__init__.py +18 -0
  157. crabcode_gateway/acp/__init__.py +13 -0
  158. crabcode_gateway/acp/agent.py +1581 -0
  159. crabcode_gateway/acp/session.py +114 -0
  160. crabcode_gateway/acp/transport.py +39 -0
  161. crabcode_gateway/acp/types.py +90 -0
  162. crabcode_gateway/adapter.py +30 -0
  163. crabcode_gateway/auth.py +139 -0
  164. crabcode_gateway/computer_use.py +364 -0
  165. crabcode_gateway/document_engine.py +637 -0
  166. crabcode_gateway/document_worker.py +301 -0
  167. crabcode_gateway/event_bus.py +655 -0
  168. crabcode_gateway/grpc/__init__.py +0 -0
  169. crabcode_gateway/grpc/crabcode_pb2.py +70 -0
  170. crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
  171. crabcode_gateway/grpc/proto/crabcode.proto +160 -0
  172. crabcode_gateway/grpc/server.py +765 -0
  173. crabcode_gateway/middleware.py +344 -0
  174. crabcode_gateway/protocol.py +17 -0
  175. crabcode_gateway/routes/__init__.py +0 -0
  176. crabcode_gateway/routes/agent.py +392 -0
  177. crabcode_gateway/routes/auth.py +150 -0
  178. crabcode_gateway/routes/computer_use.py +101 -0
  179. crabcode_gateway/routes/config.py +1698 -0
  180. crabcode_gateway/routes/document.py +1810 -0
  181. crabcode_gateway/routes/event.py +3801 -0
  182. crabcode_gateway/routes/git_review.py +232 -0
  183. crabcode_gateway/routes/health.py +14 -0
  184. crabcode_gateway/routes/peer.py +78 -0
  185. crabcode_gateway/routes/permission.py +121 -0
  186. crabcode_gateway/routes/schedule.py +300 -0
  187. crabcode_gateway/routes/session.py +1496 -0
  188. crabcode_gateway/routes/snapshot.py +211 -0
  189. crabcode_gateway/routes/tasks.py +318 -0
  190. crabcode_gateway/routes/team.py +392 -0
  191. crabcode_gateway/routes/usage.py +49 -0
  192. crabcode_gateway/routes/workspace.py +299 -0
  193. crabcode_gateway/safe_file.py +63 -0
  194. crabcode_gateway/schemas.py +1820 -0
  195. crabcode_gateway/server.py +514 -0
  196. crabcode_gateway/session_registry.py +36 -0
  197. crabcode_gateway/task_registry.py +708 -0
  198. crabcode_gateway/turn_details.py +60 -0
  199. crabcode_search/__init__.py +11 -0
  200. crabcode_search/background.py +368 -0
  201. crabcode_search/chunker.py +517 -0
  202. crabcode_search/embedder.py +497 -0
  203. crabcode_search/indexer.py +215 -0
  204. crabcode_search/store.py +222 -0
  205. crabcode_search/tool.py +268 -0
@@ -0,0 +1,239 @@
1
+ """Keyboard-driven history browser using the CLI's terminal-native palette."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import os
7
+ import time
8
+ from datetime import datetime, timezone
9
+ from typing import Any
10
+
11
+ from prompt_toolkit.application import Application
12
+ from prompt_toolkit.buffer import Buffer
13
+ from prompt_toolkit.data_structures import Point
14
+ from prompt_toolkit.filters import Condition
15
+ from prompt_toolkit.key_binding import KeyBindings
16
+ from prompt_toolkit.layout import HSplit, Layout, VSplit, Window
17
+ from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl
18
+ from prompt_toolkit.styles import Style
19
+
20
+ from crabcode_core.io_worker import run_io
21
+
22
+ PICKER_STYLE = Style.from_dict({
23
+ "accent": "ansicyan bold", "selected": "ansicyan bold",
24
+ "hint": "ansibrightblack", "focused": "ansicyan bold underline",
25
+ "error": "ansired",
26
+ })
27
+
28
+
29
+ def _timestamp(value: Any) -> float:
30
+ try:
31
+ return float(value or 0)
32
+ except (ValueError, TypeError):
33
+ try:
34
+ parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
35
+ return parsed.replace(tzinfo=parsed.tzinfo or timezone.utc).timestamp()
36
+ except (ValueError, TypeError, OverflowError):
37
+ return 0
38
+
39
+
40
+ def _plain(value: Any) -> str:
41
+ # Transcript text is literal, single-line terminal content, never markup.
42
+ return " ".join("".join(c for c in str(value or "") if c.isprintable() or c.isspace()).split())
43
+
44
+
45
+ def _age(timestamp: float) -> str:
46
+ if not timestamp:
47
+ return "unknown"
48
+ seconds = max(0, int(time.time() - timestamp))
49
+ for unit, size in (("d", 86400), ("h", 3600), ("m", 60)):
50
+ if seconds >= size:
51
+ return f"{seconds // size}{unit} ago"
52
+ return "just now"
53
+
54
+
55
+ def _load_sessions(cwd: str) -> list[dict[str, Any]]:
56
+ """Reconcile local JSONL history and include the global metadata index."""
57
+ from crabcode_core.session.meta_db import SessionMetaStore
58
+ from crabcode_core.session.storage import SessionStorage
59
+
60
+ local = SessionStorage.list_sessions(cwd)
61
+ store = SessionMetaStore()
62
+ try:
63
+ # SQLite's -1 means no limit: older history must remain searchable.
64
+ indexed = store.list_recent(limit=-1)
65
+ finally:
66
+ store.close()
67
+ # Local storage also checks durable archive markers; do not reintroduce
68
+ # rows excluded by that reconciliation from the metadata index.
69
+ rows = {r["id"]: r for r in indexed if os.path.normcase(r["cwd"]) != os.path.normcase(cwd)}
70
+ by_id = {r["id"]: r for r in indexed}
71
+ for row in local:
72
+ sid = row["session_id"]
73
+ rows[sid] = {
74
+ **by_id.get(sid, {}), **row, "id": sid, "cwd": cwd,
75
+ "updated_at": _timestamp(row.get("modified")),
76
+ "created_at": _timestamp(row.get("created_at")),
77
+ }
78
+ return list(rows.values())
79
+
80
+
81
+ class SessionPicker:
82
+ def __init__(self, cwd: str, current_session_id: str = "") -> None:
83
+ self.cwd = os.path.abspath(cwd)
84
+ self.current_session_id = current_session_id
85
+ self.rows: list[dict[str, Any]] = []
86
+ self.matches: list[dict[str, Any]] = []
87
+ self.selected = 0
88
+ self.all_projects = False
89
+ self.sort_key = "updated_at"
90
+ self.loading = True
91
+ self.error = ""
92
+ self.query = Buffer(on_text_changed=lambda _: self.refresh())
93
+ self.search = BufferControl(buffer=self.query)
94
+ self.options = FormattedTextControl(self._options, focusable=True)
95
+ self.list_control = FormattedTextControl(
96
+ self._list, get_cursor_position=lambda: Point(x=0, y=self.selected),
97
+ )
98
+ self.list_window = Window(
99
+ self.list_control, wrap_lines=False, always_hide_cursor=True,
100
+ )
101
+ self.option_index = 0
102
+ bindings = KeyBindings()
103
+
104
+ @bindings.add("escape", eager=True)
105
+ @bindings.add("c-c", eager=True)
106
+ @bindings.add("c-d", eager=True)
107
+ def cancel(event: Any) -> None:
108
+ event.app.exit(result=None)
109
+
110
+ @bindings.add("enter", eager=True)
111
+ def confirm(event: Any) -> None:
112
+ if self.matches:
113
+ event.app.exit(result=self.matches[self.selected]["id"])
114
+
115
+ @bindings.add("up")
116
+ @bindings.add("down")
117
+ @bindings.add("pageup")
118
+ @bindings.add("pagedown")
119
+ def navigate(event: Any) -> None:
120
+ key = event.key_sequence[0].key
121
+ page = max(1, self.list_window.render_info.window_height - 1) if self.list_window.render_info else 10
122
+ delta = {"up": -1, "down": 1, "pageup": -page, "pagedown": page}[key]
123
+ self.selected = max(0, min(len(self.matches) - 1, self.selected + delta))
124
+
125
+ @bindings.add("tab")
126
+ @bindings.add("s-tab")
127
+ def focus(event: Any) -> None:
128
+ current = self.option_index + 1 if event.app.layout.has_focus(self.options) else 0
129
+ current = (current + (-1 if event.key_sequence[0].key == "s-tab" else 1)) % 3
130
+ if current == 0:
131
+ event.app.layout.focus(self.search)
132
+ else:
133
+ self.option_index = current - 1
134
+ event.app.layout.focus(self.options)
135
+
136
+ @bindings.add("left", filter=Condition(lambda: self.app.layout.has_focus(self.options)))
137
+ @bindings.add("right", filter=Condition(lambda: self.app.layout.has_focus(self.options)))
138
+ @bindings.add("space", filter=Condition(lambda: self.app.layout.has_focus(self.options)))
139
+ def change_option(event: Any) -> None:
140
+ if self.option_index == 0:
141
+ self.all_projects = not self.all_projects
142
+ else:
143
+ self.sort_key = "created_at" if self.sort_key == "updated_at" else "updated_at"
144
+ self.refresh()
145
+
146
+ self.app: Application[str | None] = Application(
147
+ layout=Layout(HSplit([
148
+ Window(FormattedTextControl([("class:accent", " ╭─ CrabCode · Resume session")]), height=1),
149
+ Window(height=1),
150
+ VSplit([
151
+ Window(FormattedTextControl([("class:accent", " Search ❯ ")]), width=11),
152
+ Window(self.search, height=1, wrap_lines=False),
153
+ ], height=1),
154
+ Window(self.options, height=1),
155
+ Window(height=1),
156
+ self.list_window,
157
+ Window(FormattedTextControl(self._details), height=2, wrap_lines=False),
158
+ Window(char="─", height=1, style="class:hint"),
159
+ Window(FormattedTextControl(self._footer), height=2, wrap_lines=True),
160
+ ]), focused_element=self.search),
161
+ key_bindings=bindings, full_screen=True, erase_when_done=True,
162
+ style=PICKER_STYLE,
163
+ )
164
+
165
+ def refresh(self) -> None:
166
+ terms = self.query.text.casefold().split()
167
+ self.matches = [r for r in self.rows if r["id"] != self.current_session_id
168
+ and (self.all_projects or os.path.normcase(r["cwd"]) == os.path.normcase(self.cwd))
169
+ and all(term in " ".join(str(r.get(k, "")) for k in
170
+ ("title", "first_user_message", "preview", "id", "cwd")).casefold() for term in terms)]
171
+ self.matches.sort(key=lambda r: (_timestamp(r.get(self.sort_key)), r["id"]), reverse=True)
172
+ self.selected = 0
173
+ self.app.invalidate()
174
+
175
+ def _options(self) -> list[tuple[str, str]]:
176
+ focused = self.app.layout.has_focus(self.options)
177
+ return [
178
+ ("class:focused" if focused and self.option_index == 0 else "class:hint",
179
+ f" Project: {'All' if self.all_projects else 'Cwd'}"),
180
+ ("class:focused" if focused and self.option_index == 1 else "class:hint",
181
+ f" Sort: {'Updated' if self.sort_key == 'updated_at' else 'Created'}"),
182
+ ]
183
+
184
+ def _list(self) -> list[tuple[str, str]]:
185
+ if self.loading:
186
+ return [("class:hint", " Loading history…")]
187
+ if self.error:
188
+ return [("class:error", f" Could not load history: {_plain(self.error)}")]
189
+ if not self.matches:
190
+ return [("class:hint", " No matching sessions. Clear search or switch Project to All.")]
191
+ fragments = []
192
+ for i, row in enumerate(self.matches):
193
+ style = "class:selected" if i == self.selected else ""
194
+ title = _plain(row.get("title") or row.get("first_user_message") or row.get("preview") or "Untitled session")
195
+ fragments.extend([
196
+ (style, " ❯ " if i == self.selected else " "),
197
+ (style if i == self.selected else "class:hint", f"{_age(_timestamp(row.get(self.sort_key))):>9} "),
198
+ (style, title + ("\n" if i < len(self.matches) - 1 else "")),
199
+ ])
200
+ return fragments
201
+
202
+ def _details(self) -> list[tuple[str, str]]:
203
+ if not self.matches:
204
+ return []
205
+ row = self.matches[self.selected]
206
+ return [("class:hint", f" {_plain(row.get('title') or row.get('preview') or row.get('first_user_message'))}\n"
207
+ f" {_plain(row['cwd'])} · {row['id'][:8]} · {_plain(row.get('model'))}")]
208
+
209
+ def _footer(self) -> list[tuple[str, str]]:
210
+ count = len(self.matches)
211
+ return [("class:hint", f" ↑↓ browse · PgUp/PgDn page · Enter resume · Esc/Ctrl+C cancel {self.selected + 1 if count else 0}/{count}\n"
212
+ " Type to search · Tab focus project/sort · ←→ change option")]
213
+
214
+ async def run(self) -> str | None:
215
+ async def load() -> None:
216
+ try:
217
+ self.rows = await run_io(__name__, "_load_sessions", self.cwd)
218
+ except Exception as exc:
219
+ self.error = str(exc)
220
+ finally:
221
+ self.loading = False
222
+ self.refresh()
223
+
224
+ task: asyncio.Task[None] | None = None
225
+
226
+ def start() -> None:
227
+ nonlocal task
228
+ task = asyncio.create_task(load())
229
+
230
+ try:
231
+ return await self.app.run_async(pre_run=start)
232
+ finally:
233
+ if task is not None:
234
+ task.cancel()
235
+ await asyncio.gather(task, return_exceptions=True)
236
+
237
+
238
+ async def select_session(cwd: str, current_session_id: str = "") -> str | None:
239
+ return await SessionPicker(cwd, current_session_id).run()
@@ -0,0 +1,7 @@
1
+ """CrabCode Core - AI coding assistant engine."""
2
+
3
+ from ._version import VERSION
4
+
5
+ __version__ = VERSION
6
+
7
+ __all__ = ["VERSION", "__version__"]
@@ -0,0 +1,3 @@
1
+ """Single source of truth for the CrabCode release version."""
2
+
3
+ VERSION = "0.1.6"
@@ -0,0 +1,68 @@
1
+ """Private stdio supervisor: keep a Windows process tree in a kill-on-close job.
2
+
3
+ The supervisor joins the job before starting the command, avoiding the race
4
+ where a short-lived launcher spawns children before it can be assigned a job.
5
+ Only stdlib imports are used; this file also runs with an isolated environment.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import ctypes
11
+ from ctypes import wintypes
12
+ import subprocess
13
+ import sys
14
+
15
+
16
+ def attach_process_tree_job() -> int:
17
+ """Own descendants until this process exits; keep the job handle open."""
18
+ class BasicLimit(ctypes.Structure):
19
+ _fields_ = [
20
+ ("process_time", ctypes.c_int64), ("job_time", ctypes.c_int64),
21
+ ("flags", wintypes.DWORD), ("min_working_set", ctypes.c_size_t),
22
+ ("max_working_set", ctypes.c_size_t), ("active_processes", wintypes.DWORD),
23
+ ("affinity", ctypes.c_size_t), ("priority", wintypes.DWORD),
24
+ ("scheduling", wintypes.DWORD),
25
+ ]
26
+
27
+ class ExtendedLimit(ctypes.Structure):
28
+ _fields_ = [
29
+ ("basic", BasicLimit), ("io", ctypes.c_uint64 * 6),
30
+ ("process_memory", ctypes.c_size_t), ("job_memory", ctypes.c_size_t),
31
+ ("peak_process_memory", ctypes.c_size_t), ("peak_job_memory", ctypes.c_size_t),
32
+ ]
33
+
34
+ kernel = ctypes.WinDLL("kernel32", use_last_error=True)
35
+ kernel.CreateJobObjectW.argtypes = [ctypes.c_void_p, wintypes.LPCWSTR]
36
+ kernel.CreateJobObjectW.restype = wintypes.HANDLE
37
+ kernel.SetInformationJobObject.argtypes = [wintypes.HANDLE, ctypes.c_int, ctypes.c_void_p, wintypes.DWORD]
38
+ kernel.SetInformationJobObject.restype = wintypes.BOOL
39
+ kernel.GetCurrentProcess.restype = wintypes.HANDLE
40
+ kernel.AssignProcessToJobObject.argtypes = [wintypes.HANDLE, wintypes.HANDLE]
41
+ kernel.AssignProcessToJobObject.restype = wintypes.BOOL
42
+ job = kernel.CreateJobObjectW(None, None) # Non-inheritable handle.
43
+ if not job:
44
+ raise ctypes.WinError(ctypes.get_last_error())
45
+ limits = ExtendedLimit()
46
+ limits.basic.flags = 0x2000 # JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
47
+ if not kernel.SetInformationJobObject(job, 9, ctypes.byref(limits), ctypes.sizeof(limits)):
48
+ raise ctypes.WinError(ctypes.get_last_error())
49
+ if not kernel.AssignProcessToJobObject(job, kernel.GetCurrentProcess()):
50
+ raise ctypes.WinError(ctypes.get_last_error())
51
+ return job
52
+
53
+
54
+ def main() -> None:
55
+ attach_process_tree_job()
56
+ code = subprocess.call(
57
+ sys.argv[1:], stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr,
58
+ creationflags=subprocess.CREATE_NO_WINDOW,
59
+ )
60
+ # Process exit closes the sole job handle and reaps any surviving children.
61
+ # Do not explicitly CloseHandle(job) while still running inside the job.
62
+ kernel = ctypes.WinDLL("kernel32", use_last_error=True)
63
+ kernel.ExitProcess.argtypes = [wintypes.UINT]
64
+ kernel.ExitProcess(code & 0xFFFFFFFF)
65
+
66
+
67
+ if __name__ == "__main__":
68
+ main()