bazaar-compute-node 0.1.3__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.
- bazaar_compute_node/__init__.py +3 -0
- bazaar_compute_node/app/__init__.py +1 -0
- bazaar_compute_node/app/application.py +398 -0
- bazaar_compute_node/app/attachments.py +154 -0
- bazaar_compute_node/app/command.py +342 -0
- bazaar_compute_node/app/config.py +121 -0
- bazaar_compute_node/app/registry.py +120 -0
- bazaar_compute_node/app/transport.py +264 -0
- bazaar_compute_node/app/windows_pipe.py +463 -0
- bazaar_compute_node/app/wrapper.py +63 -0
- bazaar_compute_node/bcc.py +524 -0
- bazaar_compute_node/cli.py +382 -0
- bazaar_compute_node/contrib/__init__.py +1 -0
- bazaar_compute_node/contrib/codex_app_server/__init__.py +63 -0
- bazaar_compute_node/contrib/codex_app_server/approval.py +168 -0
- bazaar_compute_node/contrib/codex_app_server/client.py +408 -0
- bazaar_compute_node/contrib/codex_app_server/events.py +431 -0
- bazaar_compute_node/contrib/codex_app_server/plugin.py +15 -0
- bazaar_compute_node/contrib/codex_app_server/process.py +583 -0
- bazaar_compute_node/contrib/codex_app_server/protocol.py +103 -0
- bazaar_compute_node/contrib/codex_app_server/runtime.py +513 -0
- bazaar_compute_node/contrib/logging/__init__.py +5 -0
- bazaar_compute_node/contrib/logging/audit.py +61 -0
- bazaar_compute_node/contrib/logging/plugin.py +11 -0
- bazaar_compute_node/contrib/sqlite/__init__.py +14 -0
- bazaar_compute_node/contrib/sqlite/codec.py +768 -0
- bazaar_compute_node/contrib/sqlite/database.py +282 -0
- bazaar_compute_node/contrib/sqlite/migrations.py +646 -0
- bazaar_compute_node/contrib/sqlite/plugin.py +11 -0
- bazaar_compute_node/contrib/sqlite/repository.py +1059 -0
- bazaar_compute_node/contrib/wecom/__init__.py +1 -0
- bazaar_compute_node/contrib/wecom/channel.py +960 -0
- bazaar_compute_node/contrib/wecom/markdown.py +146 -0
- bazaar_compute_node/contrib/wecom/plugin.py +29 -0
- bazaar_compute_node/core/__init__.py +5 -0
- bazaar_compute_node/core/approval.py +51 -0
- bazaar_compute_node/core/audit.py +101 -0
- bazaar_compute_node/core/channel.py +121 -0
- bazaar_compute_node/core/client.py +30 -0
- bazaar_compute_node/core/command.py +85 -0
- bazaar_compute_node/core/concurrency.py +29 -0
- bazaar_compute_node/core/correlation.py +48 -0
- bazaar_compute_node/core/instruction.py +224 -0
- bazaar_compute_node/core/lifecycle.py +48 -0
- bazaar_compute_node/core/models/__init__.py +63 -0
- bazaar_compute_node/core/models/entities.py +514 -0
- bazaar_compute_node/core/models/states.py +369 -0
- bazaar_compute_node/core/observability.py +47 -0
- bazaar_compute_node/core/orchestration/__init__.py +5 -0
- bazaar_compute_node/core/orchestration/command.py +614 -0
- bazaar_compute_node/core/orchestration/services.py +135 -0
- bazaar_compute_node/core/orchestration/session.py +891 -0
- bazaar_compute_node/core/orchestration/turn.py +451 -0
- bazaar_compute_node/core/outcomes.py +51 -0
- bazaar_compute_node/core/paths.py +19 -0
- bazaar_compute_node/core/runtime.py +118 -0
- bazaar_compute_node/core/storage.py +167 -0
- bazaar_compute_node-0.1.3.dist-info/METADATA +178 -0
- bazaar_compute_node-0.1.3.dist-info/RECORD +62 -0
- bazaar_compute_node-0.1.3.dist-info/WHEEL +4 -0
- bazaar_compute_node-0.1.3.dist-info/entry_points.txt +15 -0
- bazaar_compute_node-0.1.3.dist-info/licenses/LICENSE +613 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import ctypes
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
10
|
+
from ctypes import wintypes
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
from urllib.parse import urlsplit
|
|
14
|
+
|
|
15
|
+
RequestHandler = Callable[[Mapping[str, object]], Awaitable[Mapping[str, object]]]
|
|
16
|
+
|
|
17
|
+
_ERROR_ALREADY_EXISTS = 183
|
|
18
|
+
_ERROR_BROKEN_PIPE = 109
|
|
19
|
+
_ERROR_FILE_NOT_FOUND = 2
|
|
20
|
+
_ERROR_PIPE_BUSY = 231
|
|
21
|
+
_ERROR_PIPE_CONNECTED = 535
|
|
22
|
+
_GENERIC_READ = 0x80000000
|
|
23
|
+
_GENERIC_WRITE = 0x40000000
|
|
24
|
+
_OPEN_EXISTING = 3
|
|
25
|
+
_FILE_ATTRIBUTE_NORMAL = 0x80
|
|
26
|
+
_PIPE_ACCESS_DUPLEX = 0x00000003
|
|
27
|
+
_PIPE_TYPE_BYTE = 0x00000000
|
|
28
|
+
_PIPE_READMODE_BYTE = 0x00000000
|
|
29
|
+
_PIPE_WAIT = 0x00000000
|
|
30
|
+
_PIPE_REJECT_REMOTE_CLIENTS = 0x00000008
|
|
31
|
+
_PIPE_UNLIMITED_INSTANCES = 255
|
|
32
|
+
_PIPE_BUFFER_SIZE = 64 * 1024
|
|
33
|
+
_MAX_MESSAGE_SIZE = 1024 * 1024
|
|
34
|
+
_INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value
|
|
35
|
+
|
|
36
|
+
_WinDLL = getattr(ctypes, "WinDLL") # noqa: B009
|
|
37
|
+
_get_last_error = getattr(ctypes, "get_last_error") # noqa: B009
|
|
38
|
+
_set_last_error = getattr(ctypes, "set_last_error") # noqa: B009
|
|
39
|
+
_kernel32 = _WinDLL("kernel32", use_last_error=True)
|
|
40
|
+
|
|
41
|
+
_CloseHandle = _kernel32.CloseHandle
|
|
42
|
+
_CloseHandle.argtypes = (wintypes.HANDLE,)
|
|
43
|
+
_CloseHandle.restype = wintypes.BOOL
|
|
44
|
+
|
|
45
|
+
_ConnectNamedPipe = _kernel32.ConnectNamedPipe
|
|
46
|
+
_ConnectNamedPipe.argtypes = (wintypes.HANDLE, ctypes.c_void_p)
|
|
47
|
+
_ConnectNamedPipe.restype = wintypes.BOOL
|
|
48
|
+
|
|
49
|
+
_CreateFileW = _kernel32.CreateFileW
|
|
50
|
+
_CreateFileW.argtypes = (
|
|
51
|
+
wintypes.LPCWSTR,
|
|
52
|
+
wintypes.DWORD,
|
|
53
|
+
wintypes.DWORD,
|
|
54
|
+
ctypes.c_void_p,
|
|
55
|
+
wintypes.DWORD,
|
|
56
|
+
wintypes.DWORD,
|
|
57
|
+
wintypes.HANDLE,
|
|
58
|
+
)
|
|
59
|
+
_CreateFileW.restype = wintypes.HANDLE
|
|
60
|
+
|
|
61
|
+
_CreateMutexW = _kernel32.CreateMutexW
|
|
62
|
+
_CreateMutexW.argtypes = (ctypes.c_void_p, wintypes.BOOL, wintypes.LPCWSTR)
|
|
63
|
+
_CreateMutexW.restype = wintypes.HANDLE
|
|
64
|
+
|
|
65
|
+
_CreateNamedPipeW = _kernel32.CreateNamedPipeW
|
|
66
|
+
_CreateNamedPipeW.argtypes = (
|
|
67
|
+
wintypes.LPCWSTR,
|
|
68
|
+
wintypes.DWORD,
|
|
69
|
+
wintypes.DWORD,
|
|
70
|
+
wintypes.DWORD,
|
|
71
|
+
wintypes.DWORD,
|
|
72
|
+
wintypes.DWORD,
|
|
73
|
+
wintypes.DWORD,
|
|
74
|
+
ctypes.c_void_p,
|
|
75
|
+
)
|
|
76
|
+
_CreateNamedPipeW.restype = wintypes.HANDLE
|
|
77
|
+
|
|
78
|
+
_DisconnectNamedPipe = _kernel32.DisconnectNamedPipe
|
|
79
|
+
_DisconnectNamedPipe.argtypes = (wintypes.HANDLE,)
|
|
80
|
+
_DisconnectNamedPipe.restype = wintypes.BOOL
|
|
81
|
+
|
|
82
|
+
_FlushFileBuffers = _kernel32.FlushFileBuffers
|
|
83
|
+
_FlushFileBuffers.argtypes = (wintypes.HANDLE,)
|
|
84
|
+
_FlushFileBuffers.restype = wintypes.BOOL
|
|
85
|
+
|
|
86
|
+
_ReadFile = _kernel32.ReadFile
|
|
87
|
+
_ReadFile.argtypes = (
|
|
88
|
+
wintypes.HANDLE,
|
|
89
|
+
ctypes.c_void_p,
|
|
90
|
+
wintypes.DWORD,
|
|
91
|
+
ctypes.POINTER(wintypes.DWORD),
|
|
92
|
+
ctypes.c_void_p,
|
|
93
|
+
)
|
|
94
|
+
_ReadFile.restype = wintypes.BOOL
|
|
95
|
+
|
|
96
|
+
_ReleaseMutex = _kernel32.ReleaseMutex
|
|
97
|
+
_ReleaseMutex.argtypes = (wintypes.HANDLE,)
|
|
98
|
+
_ReleaseMutex.restype = wintypes.BOOL
|
|
99
|
+
|
|
100
|
+
_WaitNamedPipeW = _kernel32.WaitNamedPipeW
|
|
101
|
+
_WaitNamedPipeW.argtypes = (wintypes.LPCWSTR, wintypes.DWORD)
|
|
102
|
+
_WaitNamedPipeW.restype = wintypes.BOOL
|
|
103
|
+
|
|
104
|
+
_WriteFile = _kernel32.WriteFile
|
|
105
|
+
_WriteFile.argtypes = (
|
|
106
|
+
wintypes.HANDLE,
|
|
107
|
+
ctypes.c_void_p,
|
|
108
|
+
wintypes.DWORD,
|
|
109
|
+
ctypes.POINTER(wintypes.DWORD),
|
|
110
|
+
ctypes.c_void_p,
|
|
111
|
+
)
|
|
112
|
+
_WriteFile.restype = wintypes.BOOL
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _raise_last_error(message: str) -> None:
|
|
116
|
+
error_code = _get_last_error()
|
|
117
|
+
raise OSError(error_code, f"{message} (WinError {error_code})")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _is_invalid_handle(handle: Any) -> bool:
|
|
121
|
+
return handle is None or handle == _INVALID_HANDLE_VALUE
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _pipe_name(endpoint_path: Path | None) -> str:
|
|
125
|
+
path = endpoint_path or (Path.home() / ".bcn" / "bcn.sock")
|
|
126
|
+
identity = str(path.expanduser().resolve(strict=False)).casefold()
|
|
127
|
+
digest = hashlib.sha256(identity.encode("utf-8")).hexdigest()[:32]
|
|
128
|
+
return f"bcn-{digest}"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def named_pipe_endpoint(endpoint_path: Path | None) -> str:
|
|
132
|
+
return f"pipe://{_pipe_name(endpoint_path)}"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _pipe_path(pipe_name: str) -> str:
|
|
136
|
+
return rf"\\.\pipe\{pipe_name}"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _mutex_name(pipe_name: str) -> str:
|
|
140
|
+
return rf"Local\{pipe_name}-mutex"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _create_mutex(pipe_name: str) -> Any:
|
|
144
|
+
_set_last_error(0)
|
|
145
|
+
handle = _CreateMutexW(None, True, _mutex_name(pipe_name))
|
|
146
|
+
if _is_invalid_handle(handle):
|
|
147
|
+
_raise_last_error("CreateMutexW failed")
|
|
148
|
+
if _get_last_error() == _ERROR_ALREADY_EXISTS:
|
|
149
|
+
_CloseHandle(handle)
|
|
150
|
+
raise FileExistsError(f"Windows named mutex already exists: {pipe_name}")
|
|
151
|
+
return handle
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _release_mutex(handle: Any) -> None:
|
|
155
|
+
if _is_invalid_handle(handle):
|
|
156
|
+
return
|
|
157
|
+
_ReleaseMutex(handle)
|
|
158
|
+
_CloseHandle(handle)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _create_named_pipe(pipe_name: str) -> Any:
|
|
162
|
+
handle = _CreateNamedPipeW(
|
|
163
|
+
_pipe_path(pipe_name),
|
|
164
|
+
_PIPE_ACCESS_DUPLEX,
|
|
165
|
+
_PIPE_TYPE_BYTE
|
|
166
|
+
| _PIPE_READMODE_BYTE
|
|
167
|
+
| _PIPE_WAIT
|
|
168
|
+
| _PIPE_REJECT_REMOTE_CLIENTS,
|
|
169
|
+
_PIPE_UNLIMITED_INSTANCES,
|
|
170
|
+
_PIPE_BUFFER_SIZE,
|
|
171
|
+
_PIPE_BUFFER_SIZE,
|
|
172
|
+
0,
|
|
173
|
+
None,
|
|
174
|
+
)
|
|
175
|
+
if _is_invalid_handle(handle):
|
|
176
|
+
_raise_last_error("CreateNamedPipeW failed")
|
|
177
|
+
return handle
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _open_named_pipe(pipe_name: str) -> Any:
|
|
181
|
+
path = _pipe_path(pipe_name)
|
|
182
|
+
for _ in range(20):
|
|
183
|
+
handle = _CreateFileW(
|
|
184
|
+
path,
|
|
185
|
+
_GENERIC_READ | _GENERIC_WRITE,
|
|
186
|
+
0,
|
|
187
|
+
None,
|
|
188
|
+
_OPEN_EXISTING,
|
|
189
|
+
_FILE_ATTRIBUTE_NORMAL,
|
|
190
|
+
None,
|
|
191
|
+
)
|
|
192
|
+
if not _is_invalid_handle(handle):
|
|
193
|
+
return handle
|
|
194
|
+
error_code = _get_last_error()
|
|
195
|
+
if error_code != _ERROR_PIPE_BUSY:
|
|
196
|
+
if error_code == _ERROR_FILE_NOT_FOUND:
|
|
197
|
+
raise FileNotFoundError(path)
|
|
198
|
+
_raise_last_error("CreateFileW for named pipe failed")
|
|
199
|
+
if not _WaitNamedPipeW(path, 100):
|
|
200
|
+
_raise_last_error("WaitNamedPipeW failed")
|
|
201
|
+
raise TimeoutError(f"named pipe did not become available: {path}")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _read_message(handle: Any) -> bytes:
|
|
205
|
+
chunks: list[bytes] = []
|
|
206
|
+
size = 0
|
|
207
|
+
while size <= _MAX_MESSAGE_SIZE:
|
|
208
|
+
buffer = ctypes.create_string_buffer(4096)
|
|
209
|
+
count = wintypes.DWORD()
|
|
210
|
+
if not _ReadFile(handle, buffer, len(buffer), ctypes.byref(count), None):
|
|
211
|
+
error_code = _get_last_error()
|
|
212
|
+
if error_code == _ERROR_BROKEN_PIPE:
|
|
213
|
+
return b""
|
|
214
|
+
_raise_last_error("ReadFile from named pipe failed")
|
|
215
|
+
if count.value == 0:
|
|
216
|
+
return b""
|
|
217
|
+
chunk = buffer.raw[: count.value]
|
|
218
|
+
chunks.append(chunk)
|
|
219
|
+
size += len(chunk)
|
|
220
|
+
payload = b"".join(chunks)
|
|
221
|
+
line_end = payload.find(b"\n")
|
|
222
|
+
if line_end >= 0:
|
|
223
|
+
return payload[:line_end]
|
|
224
|
+
raise ValueError("named pipe request is too large")
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _write_message(handle: Any, payload: bytes) -> None:
|
|
228
|
+
offset = 0
|
|
229
|
+
while offset < len(payload):
|
|
230
|
+
buffer = ctypes.create_string_buffer(payload[offset:])
|
|
231
|
+
count = wintypes.DWORD()
|
|
232
|
+
if not _WriteFile(
|
|
233
|
+
handle,
|
|
234
|
+
buffer,
|
|
235
|
+
len(payload) - offset,
|
|
236
|
+
ctypes.byref(count),
|
|
237
|
+
None,
|
|
238
|
+
):
|
|
239
|
+
_raise_last_error("WriteFile to named pipe failed")
|
|
240
|
+
if count.value == 0:
|
|
241
|
+
raise ConnectionError("named pipe accepted no response bytes")
|
|
242
|
+
offset += count.value
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def request_named_pipe(
|
|
246
|
+
endpoint: str, payload: Mapping[str, object]
|
|
247
|
+
) -> Mapping[str, object]:
|
|
248
|
+
parsed = urlsplit(endpoint)
|
|
249
|
+
if (
|
|
250
|
+
parsed.scheme != "pipe"
|
|
251
|
+
or not parsed.netloc
|
|
252
|
+
or parsed.path
|
|
253
|
+
or parsed.query
|
|
254
|
+
or parsed.fragment
|
|
255
|
+
):
|
|
256
|
+
raise ValueError(f"invalid Windows named pipe endpoint: {endpoint}")
|
|
257
|
+
handle = _open_named_pipe(parsed.netloc)
|
|
258
|
+
try:
|
|
259
|
+
request = (
|
|
260
|
+
json.dumps(
|
|
261
|
+
dict(payload), ensure_ascii=False, separators=(",", ":")
|
|
262
|
+
).encode()
|
|
263
|
+
+ b"\n"
|
|
264
|
+
)
|
|
265
|
+
_write_message(handle, request)
|
|
266
|
+
response_line = _read_message(handle)
|
|
267
|
+
if not response_line:
|
|
268
|
+
raise ConnectionError("named pipe closed without a response")
|
|
269
|
+
response = json.loads(response_line)
|
|
270
|
+
if not isinstance(response, dict):
|
|
271
|
+
raise TypeError("named pipe response must be a JSON object")
|
|
272
|
+
return response
|
|
273
|
+
finally:
|
|
274
|
+
_CloseHandle(handle)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _wake_named_pipe(pipe_name: str) -> None:
|
|
278
|
+
try:
|
|
279
|
+
handle = _open_named_pipe(pipe_name)
|
|
280
|
+
except FileNotFoundError, OSError, TimeoutError:
|
|
281
|
+
return
|
|
282
|
+
_CloseHandle(handle)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
class WindowsNamedPipeServer:
|
|
286
|
+
"""Run one JSONL request handler over a per-user named pipe."""
|
|
287
|
+
|
|
288
|
+
def __init__(
|
|
289
|
+
self,
|
|
290
|
+
handler: RequestHandler,
|
|
291
|
+
*,
|
|
292
|
+
endpoint_path: Path | None,
|
|
293
|
+
) -> None:
|
|
294
|
+
self._handler = handler
|
|
295
|
+
self._pipe_name = _pipe_name(endpoint_path)
|
|
296
|
+
self._endpoint = named_pipe_endpoint(endpoint_path)
|
|
297
|
+
self._loop: asyncio.AbstractEventLoop | None = None
|
|
298
|
+
self._mutex: Any = None
|
|
299
|
+
self._stop = threading.Event()
|
|
300
|
+
self._ready = threading.Event()
|
|
301
|
+
self._startup_errors: list[BaseException] = []
|
|
302
|
+
self._thread: threading.Thread | None = None
|
|
303
|
+
self._workers: set[threading.Thread] = set()
|
|
304
|
+
self._workers_lock = threading.Lock()
|
|
305
|
+
|
|
306
|
+
@property
|
|
307
|
+
def endpoint(self) -> str:
|
|
308
|
+
return self._endpoint
|
|
309
|
+
|
|
310
|
+
async def start(self) -> None:
|
|
311
|
+
if self._thread is not None:
|
|
312
|
+
return
|
|
313
|
+
self._mutex = _create_mutex(self._pipe_name)
|
|
314
|
+
self._loop = asyncio.get_running_loop()
|
|
315
|
+
self._stop.clear()
|
|
316
|
+
self._ready.clear()
|
|
317
|
+
self._startup_errors.clear()
|
|
318
|
+
self._thread = threading.Thread(
|
|
319
|
+
target=self._serve,
|
|
320
|
+
name="bcn-windows-named-pipe",
|
|
321
|
+
daemon=True,
|
|
322
|
+
)
|
|
323
|
+
self._thread.start()
|
|
324
|
+
ready = await asyncio.to_thread(self._ready.wait, 5)
|
|
325
|
+
if not ready or self._startup_errors:
|
|
326
|
+
error = self._startup_errors[0] if self._startup_errors else None
|
|
327
|
+
await self.stop()
|
|
328
|
+
if error is not None:
|
|
329
|
+
raise RuntimeError("Windows named pipe server failed") from error
|
|
330
|
+
raise TimeoutError("Windows named pipe server did not become ready")
|
|
331
|
+
|
|
332
|
+
async def stop(self) -> None:
|
|
333
|
+
thread = self._thread
|
|
334
|
+
if thread is None:
|
|
335
|
+
_release_mutex(self._mutex)
|
|
336
|
+
self._mutex = None
|
|
337
|
+
return
|
|
338
|
+
self._stop.set()
|
|
339
|
+
await asyncio.to_thread(_wake_named_pipe, self._pipe_name)
|
|
340
|
+
await asyncio.to_thread(thread.join, 5)
|
|
341
|
+
await asyncio.to_thread(self._join_workers, 5)
|
|
342
|
+
self._thread = None
|
|
343
|
+
self._loop = None
|
|
344
|
+
_release_mutex(self._mutex)
|
|
345
|
+
self._mutex = None
|
|
346
|
+
if thread.is_alive() or self._live_workers():
|
|
347
|
+
raise TimeoutError("Windows named pipe server did not stop")
|
|
348
|
+
|
|
349
|
+
def _serve(self) -> None:
|
|
350
|
+
try:
|
|
351
|
+
self._serve_loop()
|
|
352
|
+
except Exception as error: # noqa: BLE001
|
|
353
|
+
self._startup_errors.append(error)
|
|
354
|
+
self._ready.set()
|
|
355
|
+
|
|
356
|
+
def _serve_loop(self) -> None:
|
|
357
|
+
initialized = False
|
|
358
|
+
while not self._stop.is_set():
|
|
359
|
+
handle = _create_named_pipe(self._pipe_name)
|
|
360
|
+
if not initialized:
|
|
361
|
+
initialized = True
|
|
362
|
+
self._ready.set()
|
|
363
|
+
connected = _ConnectNamedPipe(handle, None)
|
|
364
|
+
if not connected and _get_last_error() != _ERROR_PIPE_CONNECTED:
|
|
365
|
+
if self._stop.is_set():
|
|
366
|
+
_DisconnectNamedPipe(handle)
|
|
367
|
+
_CloseHandle(handle)
|
|
368
|
+
return
|
|
369
|
+
_DisconnectNamedPipe(handle)
|
|
370
|
+
_CloseHandle(handle)
|
|
371
|
+
_raise_last_error("ConnectNamedPipe failed")
|
|
372
|
+
if self._stop.is_set():
|
|
373
|
+
_DisconnectNamedPipe(handle)
|
|
374
|
+
_CloseHandle(handle)
|
|
375
|
+
return
|
|
376
|
+
worker = threading.Thread(
|
|
377
|
+
target=self._serve_client_worker,
|
|
378
|
+
args=(handle,),
|
|
379
|
+
name="bcn-windows-named-pipe-client",
|
|
380
|
+
daemon=True,
|
|
381
|
+
)
|
|
382
|
+
with self._workers_lock:
|
|
383
|
+
self._workers.add(worker)
|
|
384
|
+
try:
|
|
385
|
+
worker.start()
|
|
386
|
+
except BaseException:
|
|
387
|
+
with self._workers_lock:
|
|
388
|
+
self._workers.discard(worker)
|
|
389
|
+
_DisconnectNamedPipe(handle)
|
|
390
|
+
_CloseHandle(handle)
|
|
391
|
+
raise
|
|
392
|
+
|
|
393
|
+
def _serve_client_worker(self, handle: Any) -> None:
|
|
394
|
+
try:
|
|
395
|
+
self._serve_client(handle)
|
|
396
|
+
except Exception: # noqa: BLE001
|
|
397
|
+
return
|
|
398
|
+
finally:
|
|
399
|
+
_DisconnectNamedPipe(handle)
|
|
400
|
+
_CloseHandle(handle)
|
|
401
|
+
with self._workers_lock:
|
|
402
|
+
self._workers.discard(threading.current_thread())
|
|
403
|
+
|
|
404
|
+
def _live_workers(self) -> tuple[threading.Thread, ...]:
|
|
405
|
+
with self._workers_lock:
|
|
406
|
+
return tuple(worker for worker in self._workers if worker.is_alive())
|
|
407
|
+
|
|
408
|
+
def _join_workers(self, timeout: float) -> None:
|
|
409
|
+
deadline = time.monotonic() + timeout
|
|
410
|
+
for worker in self._live_workers():
|
|
411
|
+
remaining = deadline - time.monotonic()
|
|
412
|
+
if remaining <= 0:
|
|
413
|
+
break
|
|
414
|
+
worker.join(remaining)
|
|
415
|
+
|
|
416
|
+
def _serve_client(self, handle: Any) -> None:
|
|
417
|
+
raw_request = _read_message(handle)
|
|
418
|
+
if not raw_request:
|
|
419
|
+
return
|
|
420
|
+
try:
|
|
421
|
+
payload = json.loads(raw_request)
|
|
422
|
+
if not isinstance(payload, dict):
|
|
423
|
+
raise TypeError("request must be a JSON object")
|
|
424
|
+
loop = self._loop
|
|
425
|
+
if loop is None:
|
|
426
|
+
raise RuntimeError("named pipe server event loop is unavailable")
|
|
427
|
+
|
|
428
|
+
async def invoke_handler() -> Mapping[str, object]:
|
|
429
|
+
return await self._handler(payload)
|
|
430
|
+
|
|
431
|
+
future = asyncio.run_coroutine_threadsafe(invoke_handler(), loop)
|
|
432
|
+
response: Mapping[str, object] = future.result()
|
|
433
|
+
except json.JSONDecodeError as error:
|
|
434
|
+
response = {
|
|
435
|
+
"ok": False,
|
|
436
|
+
"code": "INVALID_REQUEST",
|
|
437
|
+
"error": f"invalid JSON request: {error.msg}",
|
|
438
|
+
}
|
|
439
|
+
except ValueError as error:
|
|
440
|
+
response = {
|
|
441
|
+
"ok": False,
|
|
442
|
+
"code": "INVALID_REQUEST",
|
|
443
|
+
"error": str(error),
|
|
444
|
+
}
|
|
445
|
+
except Exception as error: # noqa: BLE001
|
|
446
|
+
response = {
|
|
447
|
+
"ok": False,
|
|
448
|
+
"code": "COMMAND_FAILED",
|
|
449
|
+
"error": str(error),
|
|
450
|
+
}
|
|
451
|
+
encoded_response = (
|
|
452
|
+
json.dumps(response, ensure_ascii=False, separators=(",", ":")).encode()
|
|
453
|
+
+ b"\n"
|
|
454
|
+
)
|
|
455
|
+
_write_message(handle, encoded_response)
|
|
456
|
+
_FlushFileBuffers(handle)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
__all__ = [
|
|
460
|
+
"WindowsNamedPipeServer",
|
|
461
|
+
"named_pipe_endpoint",
|
|
462
|
+
"request_named_pipe",
|
|
463
|
+
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import shlex
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _wrapper_paths(command_path: Path) -> tuple[Path, ...]:
|
|
10
|
+
if command_path.name == "bcc":
|
|
11
|
+
return (command_path,)
|
|
12
|
+
if command_path.name == "bcc.cmd":
|
|
13
|
+
return (command_path, command_path.with_name("bcc.ps1"))
|
|
14
|
+
raise ValueError(f"unsupported bcc wrapper path: {command_path}")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def install_bcc_wrapper(bin_dir: Path) -> Path:
|
|
18
|
+
"""Install the development wrapper and return the executable path."""
|
|
19
|
+
|
|
20
|
+
bin_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
21
|
+
if os.name != "nt":
|
|
22
|
+
os.chmod(bin_dir, 0o700)
|
|
23
|
+
python_executable = Path(sys.executable)
|
|
24
|
+
if os.name == "nt":
|
|
25
|
+
command_path = bin_dir / "bcc.cmd"
|
|
26
|
+
try:
|
|
27
|
+
command_path.write_text(
|
|
28
|
+
f'@echo off\r\n"{python_executable}" -m bazaar_compute_node.bcc %*\r\n',
|
|
29
|
+
encoding="utf-8",
|
|
30
|
+
)
|
|
31
|
+
(bin_dir / "bcc.ps1").write_text(
|
|
32
|
+
f'& "{python_executable}" -m bazaar_compute_node.bcc @args\n'
|
|
33
|
+
"exit $LASTEXITCODE\n",
|
|
34
|
+
encoding="utf-8",
|
|
35
|
+
)
|
|
36
|
+
except BaseException:
|
|
37
|
+
remove_bcc_wrapper(command_path)
|
|
38
|
+
raise
|
|
39
|
+
return command_path
|
|
40
|
+
|
|
41
|
+
command_path = bin_dir / "bcc"
|
|
42
|
+
try:
|
|
43
|
+
command_path.write_text(
|
|
44
|
+
"#!/bin/sh\n"
|
|
45
|
+
f"exec {shlex.quote(str(python_executable))} "
|
|
46
|
+
'-m bazaar_compute_node.bcc "$@"\n',
|
|
47
|
+
encoding="utf-8",
|
|
48
|
+
)
|
|
49
|
+
os.chmod(command_path, 0o700)
|
|
50
|
+
except BaseException:
|
|
51
|
+
remove_bcc_wrapper(command_path)
|
|
52
|
+
raise
|
|
53
|
+
return command_path
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def remove_bcc_wrapper(command_path: Path) -> None:
|
|
57
|
+
"""Remove only the wrapper files generated for one node instance."""
|
|
58
|
+
|
|
59
|
+
for path in _wrapper_paths(command_path):
|
|
60
|
+
try:
|
|
61
|
+
path.unlink()
|
|
62
|
+
except FileNotFoundError:
|
|
63
|
+
pass
|