buildai-cli 0.3.62__tar.gz → 0.3.63__tar.gz
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.
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/PKG-INFO +1 -1
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/auth_local.py +21 -6
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/db_broker.py +140 -6
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/pyproject.toml +1 -1
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/.gitignore +0 -0
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/AGENTS.md +0 -0
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/CLAUDE.md +0 -0
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/buildai_bootstrap.py +0 -0
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/__init__.py +0 -0
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/_has_core.py +0 -0
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/commands/__init__.py +0 -0
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/commands/api_proxy.py +0 -0
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/commands/auth.py +0 -0
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/commands/db/__init__.py +0 -0
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/commands/db/broker.py +0 -0
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/commands/db/common.py +0 -0
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/commands/db/migrate.py +0 -0
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/commands/db/query.py +0 -0
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/commands/db/schema.py +0 -0
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/commands/db/status.py +0 -0
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/commands/dev.py +0 -0
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/commands/doctor.py +0 -0
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/commands/gigcamera.py +0 -0
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/commands/processing.py +0 -0
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/config.py +0 -0
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/console.py +0 -0
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/context.py +0 -0
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/guard.py +0 -0
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/internal_api.py +0 -0
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/main.py +0 -0
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/nl_query/__init__.py +0 -0
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/nl_query/dataset_tools.py +0 -0
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/ops_init.py +0 -0
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/output.py +0 -0
- {buildai_cli-0.3.62 → buildai_cli-0.3.63}/cli/pagination.py +0 -0
|
@@ -353,12 +353,27 @@ def _fixed_targets_for_environment(
|
|
|
353
353
|
def _run_command(argv: list[str], *, timeout: int = 20) -> CommandResult:
|
|
354
354
|
"""Run one external command with bounded time and normalized output fields."""
|
|
355
355
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
356
|
+
try:
|
|
357
|
+
completed = subprocess.run(
|
|
358
|
+
argv,
|
|
359
|
+
capture_output=True,
|
|
360
|
+
text=True,
|
|
361
|
+
timeout=timeout,
|
|
362
|
+
)
|
|
363
|
+
except FileNotFoundError as exc:
|
|
364
|
+
return CommandResult(
|
|
365
|
+
argv=tuple(argv),
|
|
366
|
+
returncode=127,
|
|
367
|
+
stdout="",
|
|
368
|
+
stderr=f"{argv[0]} is not installed or is not on PATH: {exc}",
|
|
369
|
+
)
|
|
370
|
+
except subprocess.TimeoutExpired as exc:
|
|
371
|
+
return CommandResult(
|
|
372
|
+
argv=tuple(argv),
|
|
373
|
+
returncode=124,
|
|
374
|
+
stdout=(exc.stdout or "").strip() if isinstance(exc.stdout, str) else "",
|
|
375
|
+
stderr=(exc.stderr or "").strip() if isinstance(exc.stderr, str) else "Command timed out.",
|
|
376
|
+
)
|
|
362
377
|
return CommandResult(
|
|
363
378
|
argv=tuple(argv),
|
|
364
379
|
returncode=completed.returncode,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import ctypes
|
|
6
6
|
import hashlib
|
|
7
7
|
import json
|
|
8
8
|
import os
|
|
@@ -20,6 +20,11 @@ from typing import Any, Iterator
|
|
|
20
20
|
import asyncpg
|
|
21
21
|
from infra.settings import Settings
|
|
22
22
|
|
|
23
|
+
if os.name == "nt":
|
|
24
|
+
import msvcrt
|
|
25
|
+
else:
|
|
26
|
+
import fcntl
|
|
27
|
+
|
|
23
28
|
REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
24
29
|
LEGACY_STATE_DIR = REPO_ROOT / ".buildai" / "db-brokers"
|
|
25
30
|
STATE_DIR = Path(os.getenv("BUILDAI_DB_BROKER_STATE_DIR", Path.home() / ".buildai" / "db-brokers"))
|
|
@@ -192,6 +197,9 @@ def proxy_is_listening(*, host: str, port: int) -> bool:
|
|
|
192
197
|
def process_is_running(pid: int) -> bool:
|
|
193
198
|
"""Return whether the broker PID still exists."""
|
|
194
199
|
|
|
200
|
+
if os.name == "nt":
|
|
201
|
+
return _windows_process_is_running(pid)
|
|
202
|
+
|
|
195
203
|
try:
|
|
196
204
|
os.kill(pid, 0)
|
|
197
205
|
except ProcessLookupError:
|
|
@@ -201,18 +209,59 @@ def process_is_running(pid: int) -> bool:
|
|
|
201
209
|
return True
|
|
202
210
|
|
|
203
211
|
|
|
212
|
+
def _windows_process_is_running(pid: int) -> bool:
|
|
213
|
+
"""Check a Windows process handle without sending a signal to the process."""
|
|
214
|
+
|
|
215
|
+
if pid <= 0:
|
|
216
|
+
return False
|
|
217
|
+
|
|
218
|
+
synchronize = 0x00100000
|
|
219
|
+
wait_timeout = 0x00000102
|
|
220
|
+
kernel32 = ctypes.windll.kernel32
|
|
221
|
+
handle = kernel32.OpenProcess(synchronize, False, int(pid))
|
|
222
|
+
if not handle:
|
|
223
|
+
return False
|
|
224
|
+
try:
|
|
225
|
+
return kernel32.WaitForSingleObject(handle, 0) == wait_timeout
|
|
226
|
+
finally:
|
|
227
|
+
kernel32.CloseHandle(handle)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _lock_file(handle: Any) -> None:
|
|
231
|
+
"""Acquire an exclusive machine-local lock for the opened lock file."""
|
|
232
|
+
|
|
233
|
+
if os.name == "nt":
|
|
234
|
+
handle.seek(0)
|
|
235
|
+
handle.write(b"\0")
|
|
236
|
+
handle.flush()
|
|
237
|
+
handle.seek(0)
|
|
238
|
+
msvcrt.locking(handle.fileno(), msvcrt.LK_LOCK, 1)
|
|
239
|
+
return
|
|
240
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _unlock_file(handle: Any) -> None:
|
|
244
|
+
"""Release a lock previously acquired by ``_lock_file``."""
|
|
245
|
+
|
|
246
|
+
if os.name == "nt":
|
|
247
|
+
handle.seek(0)
|
|
248
|
+
msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
|
|
249
|
+
return
|
|
250
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
|
|
251
|
+
|
|
252
|
+
|
|
204
253
|
@contextmanager
|
|
205
254
|
def _broker_lock(config: BrokerConfig) -> Iterator[None]:
|
|
206
255
|
"""Serialize local broker startup for one profile identity on this machine."""
|
|
207
256
|
|
|
208
257
|
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
|
209
258
|
lock_path = STATE_DIR / f"{config.identity}.lock"
|
|
210
|
-
with lock_path.open("
|
|
211
|
-
|
|
259
|
+
with lock_path.open("a+b") as handle:
|
|
260
|
+
_lock_file(handle)
|
|
212
261
|
try:
|
|
213
262
|
yield
|
|
214
263
|
finally:
|
|
215
|
-
|
|
264
|
+
_unlock_file(handle)
|
|
216
265
|
|
|
217
266
|
|
|
218
267
|
def _state_paths(config: BrokerConfig) -> tuple[Path, ...]:
|
|
@@ -286,6 +335,9 @@ def _write_broker_state(config: BrokerConfig, state: BrokerState) -> None:
|
|
|
286
335
|
def _process_command(pid: int) -> list[str]:
|
|
287
336
|
"""Return argv-like command text for a local PID when the OS exposes it."""
|
|
288
337
|
|
|
338
|
+
if os.name == "nt":
|
|
339
|
+
return _windows_process_command(pid)
|
|
340
|
+
|
|
289
341
|
try:
|
|
290
342
|
completed = subprocess.run(
|
|
291
343
|
["ps", "-p", str(pid), "-o", "command="],
|
|
@@ -307,9 +359,44 @@ def _process_command(pid: int) -> list[str]:
|
|
|
307
359
|
return raw.split()
|
|
308
360
|
|
|
309
361
|
|
|
362
|
+
def _windows_process_command(pid: int) -> list[str]:
|
|
363
|
+
"""Return command-line tokens for a Windows process when PowerShell exposes it."""
|
|
364
|
+
|
|
365
|
+
try:
|
|
366
|
+
completed = subprocess.run(
|
|
367
|
+
[
|
|
368
|
+
"powershell",
|
|
369
|
+
"-NoProfile",
|
|
370
|
+
"-Command",
|
|
371
|
+
(
|
|
372
|
+
"$p = Get-CimInstance Win32_Process -Filter "
|
|
373
|
+
f"'ProcessId = {int(pid)}'; if ($p) {{ $p.CommandLine }}"
|
|
374
|
+
),
|
|
375
|
+
],
|
|
376
|
+
capture_output=True,
|
|
377
|
+
text=True,
|
|
378
|
+
timeout=2,
|
|
379
|
+
check=False,
|
|
380
|
+
)
|
|
381
|
+
except Exception:
|
|
382
|
+
return []
|
|
383
|
+
if completed.returncode != 0:
|
|
384
|
+
return []
|
|
385
|
+
raw = completed.stdout.strip()
|
|
386
|
+
if not raw:
|
|
387
|
+
return []
|
|
388
|
+
try:
|
|
389
|
+
return shlex.split(raw, posix=False)
|
|
390
|
+
except ValueError:
|
|
391
|
+
return raw.split()
|
|
392
|
+
|
|
393
|
+
|
|
310
394
|
def _listening_pids(*, host: str, port: int) -> list[int]:
|
|
311
395
|
"""Return local PIDs listening on a TCP port when OS tools expose them."""
|
|
312
396
|
|
|
397
|
+
if os.name == "nt":
|
|
398
|
+
return _windows_listening_pids(host=host, port=port)
|
|
399
|
+
|
|
313
400
|
if shutil.which("lsof") is not None:
|
|
314
401
|
try:
|
|
315
402
|
completed = subprocess.run(
|
|
@@ -355,6 +442,41 @@ def _listening_pids(*, host: str, port: int) -> list[int]:
|
|
|
355
442
|
return []
|
|
356
443
|
|
|
357
444
|
|
|
445
|
+
def _windows_listening_pids(*, host: str, port: int) -> list[int]:
|
|
446
|
+
"""Return Windows PIDs listening on a local TCP port via netstat."""
|
|
447
|
+
|
|
448
|
+
try:
|
|
449
|
+
completed = subprocess.run(
|
|
450
|
+
["netstat", "-ano", "-p", "tcp"],
|
|
451
|
+
capture_output=True,
|
|
452
|
+
text=True,
|
|
453
|
+
timeout=2,
|
|
454
|
+
check=False,
|
|
455
|
+
)
|
|
456
|
+
except Exception:
|
|
457
|
+
return []
|
|
458
|
+
if completed.returncode != 0:
|
|
459
|
+
return []
|
|
460
|
+
|
|
461
|
+
pids: list[int] = []
|
|
462
|
+
port_suffix = f":{port}"
|
|
463
|
+
for line in completed.stdout.splitlines():
|
|
464
|
+
parts = line.split()
|
|
465
|
+
if len(parts) < 5 or parts[0].upper() != "TCP":
|
|
466
|
+
continue
|
|
467
|
+
local_address = parts[1]
|
|
468
|
+
state = parts[3].upper()
|
|
469
|
+
if state != "LISTENING" or not local_address.endswith(port_suffix):
|
|
470
|
+
continue
|
|
471
|
+
if host not in ("0.0.0.0", "::", "localhost") and not local_address.startswith(host):
|
|
472
|
+
continue
|
|
473
|
+
try:
|
|
474
|
+
pids.append(int(parts[-1]))
|
|
475
|
+
except ValueError:
|
|
476
|
+
continue
|
|
477
|
+
return pids
|
|
478
|
+
|
|
479
|
+
|
|
358
480
|
def _listener_processes(*, host: str, port: int) -> list[ListenerProcess]:
|
|
359
481
|
"""Return process metadata for listeners on the broker endpoint."""
|
|
360
482
|
|
|
@@ -382,7 +504,8 @@ def _is_matching_proxy_process(process: ListenerProcess, config: BrokerConfig) -
|
|
|
382
504
|
command = process.command
|
|
383
505
|
if not command:
|
|
384
506
|
return False
|
|
385
|
-
|
|
507
|
+
executable_name = Path(command[0].strip('"')).name.lower()
|
|
508
|
+
if executable_name not in {"alloydb-auth-proxy", "alloydb-auth-proxy.exe"}:
|
|
386
509
|
return False
|
|
387
510
|
if config.instance_uri not in command:
|
|
388
511
|
return False
|
|
@@ -435,6 +558,17 @@ def _broker_command(config: BrokerConfig, *, proxy_binary: str) -> list[str]:
|
|
|
435
558
|
return command
|
|
436
559
|
|
|
437
560
|
|
|
561
|
+
def _proxy_popen_kwargs() -> dict[str, Any]:
|
|
562
|
+
"""Return process-detach kwargs appropriate for the current platform."""
|
|
563
|
+
|
|
564
|
+
if os.name != "nt":
|
|
565
|
+
return {"start_new_session": True}
|
|
566
|
+
creationflags = 0
|
|
567
|
+
creationflags |= getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
|
|
568
|
+
creationflags |= getattr(subprocess, "DETACHED_PROCESS", 0)
|
|
569
|
+
return {"creationflags": creationflags}
|
|
570
|
+
|
|
571
|
+
|
|
438
572
|
def _recent_broker_log(config: BrokerConfig, *, max_bytes: int = 12000) -> str:
|
|
439
573
|
"""Return the recent proxy log text that explains connection resets."""
|
|
440
574
|
|
|
@@ -565,8 +699,8 @@ def ensure_broker(config: BrokerConfig) -> BrokerState:
|
|
|
565
699
|
command,
|
|
566
700
|
stdout=log_file,
|
|
567
701
|
stderr=log_file,
|
|
568
|
-
start_new_session=True,
|
|
569
702
|
env=os.environ.copy(),
|
|
703
|
+
**_proxy_popen_kwargs(),
|
|
570
704
|
)
|
|
571
705
|
|
|
572
706
|
deadline = time.monotonic() + 15.0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|