isolate 0.26.4__tar.gz → 0.26.6__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.
- {isolate-0.26.4/src/isolate.egg-info → isolate-0.26.6}/PKG-INFO +1 -1
- {isolate-0.26.4 → isolate-0.26.6}/pyproject.toml +5 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/_isolate_version.py +3 -3
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/backends/common.py +6 -4
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/connections/grpc/_base.py +15 -3
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/logger.py +6 -1
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/server/server.py +99 -16
- {isolate-0.26.4 → isolate-0.26.6/src/isolate.egg-info}/PKG-INFO +1 -1
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate.egg-info/SOURCES.txt +1 -0
- {isolate-0.26.4 → isolate-0.26.6}/tests/test_connections_grpc_base.py +35 -11
- isolate-0.26.6/tests/test_logged_io_pipe.py +132 -0
- {isolate-0.26.4 → isolate-0.26.6}/.github/workflows/claude-code-review.yml +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/.github/workflows/claude.yml +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/.github/workflows/lint.yml +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/.github/workflows/release.yml +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/.github/workflows/test.yml +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/.gitignore +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/.pre-commit-config.yaml +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/LICENSE +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/README.md +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/setup.cfg +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/__init__.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/_version.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/backends/__init__.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/backends/_base.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/backends/conda.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/backends/container.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/backends/local.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/backends/pyenv.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/backends/remote.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/backends/settings.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/backends/virtualenv.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/common/__init__.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/common/timestamp.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/connections/__init__.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/connections/_local/__init__.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/connections/_local/_base.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/connections/_local/agent_startup.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/connections/common.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/connections/grpc/__init__.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/connections/grpc/agent.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/connections/grpc/configuration.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/connections/grpc/definitions/__init__.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/connections/grpc/definitions/agent.proto +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/connections/grpc/definitions/agent_pb2.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/connections/grpc/definitions/agent_pb2.pyi +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/connections/grpc/definitions/agent_pb2_grpc.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/connections/grpc/definitions/common.proto +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/connections/grpc/definitions/common_pb2.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/connections/grpc/definitions/common_pb2.pyi +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/connections/grpc/definitions/common_pb2_grpc.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/connections/grpc/interface.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/connections/ipc/__init__.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/connections/ipc/_base.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/connections/ipc/agent.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/logs.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/py.typed +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/registry.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/server/__init__.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/server/definitions/__init__.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/server/definitions/server.proto +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/server/definitions/server_pb2.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/server/definitions/server_pb2.pyi +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/server/definitions/server_pb2_grpc.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/server/health/__init__.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/server/health/health.proto +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/server/health/health_pb2.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/server/health/health_pb2.pyi +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/server/health/health_pb2_grpc.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/server/health_server.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate/server/interface.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate.egg-info/dependency_links.txt +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate.egg-info/entry_points.txt +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate.egg-info/requires.txt +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/src/isolate.egg-info/top_level.txt +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/tests/__init__.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/tests/conftest.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/tests/test_agent_backward_compatibility.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/tests/test_backends.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/tests/test_concurrency.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/tests/test_connections.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/tests/test_isolate.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/tests/test_log.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/tests/test_log_masking.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/tests/test_logger.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/tests/test_serialization.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/tests/test_server.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/tests/test_shutdown.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/tools/Dockerfile +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/tools/agent_requirements.txt +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/tools/isolate_client.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/tools/protobuf-requirements.txt +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/tools/regen_grpc.py +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/tools/requirements.txt +0 -0
- {isolate-0.26.4 → isolate-0.26.6}/tools/test_agent_requirements.txt +0 -0
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.26.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 26,
|
|
31
|
+
__version__ = version = '0.26.6'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 26, 6)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'g6d4ff5277'
|
|
@@ -166,13 +166,15 @@ def _close_fd(fd: int) -> None:
|
|
|
166
166
|
|
|
167
167
|
|
|
168
168
|
def _unblocked_pipe() -> tuple[int, int]:
|
|
169
|
-
"""Create a
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
"""Create a pipe whose read end is non-blocking, so the reader thread in
|
|
170
|
+
`_io_observer` can `select()` + `read()` without risk of hanging on a
|
|
171
|
+
partial-line edge case. The write end stays blocking so the worker's
|
|
172
|
+
stdout/stderr applies backpressure (waits for the reader to drain the
|
|
173
|
+
kernel pipe buffer) instead of raising BlockingIOError when the buffer
|
|
174
|
+
fills up — which happens reliably with chunky tracebacks on Python 3.14."""
|
|
172
175
|
|
|
173
176
|
read_fd, write_fd = os.pipe()
|
|
174
177
|
os.set_blocking(read_fd, False)
|
|
175
|
-
os.set_blocking(write_fd, False)
|
|
176
178
|
return read_fd, write_fd
|
|
177
179
|
|
|
178
180
|
|
|
@@ -185,14 +185,26 @@ class LocalPythonGRPC(PythonExecutionBase[str], GRPCExecutionBase):
|
|
|
185
185
|
# already finished
|
|
186
186
|
return_code = self._process.returncode
|
|
187
187
|
else:
|
|
188
|
-
|
|
188
|
+
self.log(
|
|
189
|
+
"Terminating the agent process...",
|
|
190
|
+
level=LogLevel.INFO,
|
|
191
|
+
source=LogSource.BRIDGE,
|
|
192
|
+
)
|
|
189
193
|
self._process.terminate()
|
|
190
194
|
return_code = self._process.wait(
|
|
191
195
|
timeout=PROCESS_SHUTDOWN_TIMEOUT_SECONDS
|
|
192
196
|
)
|
|
193
|
-
|
|
197
|
+
self.log(
|
|
198
|
+
"Agent process shutdown gracefully",
|
|
199
|
+
level=LogLevel.INFO,
|
|
200
|
+
source=LogSource.BRIDGE,
|
|
201
|
+
)
|
|
194
202
|
except Exception as exc:
|
|
195
|
-
|
|
203
|
+
self.log(
|
|
204
|
+
f"Failed to shutdown the agent process gracefully: {exc}",
|
|
205
|
+
level=LogLevel.ERROR,
|
|
206
|
+
source=LogSource.BRIDGE,
|
|
207
|
+
)
|
|
196
208
|
self._process.kill()
|
|
197
209
|
return_code = self._process.wait()
|
|
198
210
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import os
|
|
3
|
+
import sys
|
|
3
4
|
from datetime import datetime, timezone
|
|
4
5
|
from typing import Dict
|
|
5
6
|
|
|
@@ -55,6 +56,10 @@ class IsolateLogger:
|
|
|
55
56
|
try:
|
|
56
57
|
_labels = json.loads(raw)
|
|
57
58
|
except json.JSONDecodeError:
|
|
58
|
-
|
|
59
|
+
# Write to stderr so a malformed env var does not become a
|
|
60
|
+
# constant first stdout line and break Vector's content-based
|
|
61
|
+
# file fingerprint (collision-prone first lines cause silent
|
|
62
|
+
# log skipping; see infra commit b9f0f2237).
|
|
63
|
+
sys.stderr.write("Failed to parse ISOLATE_LOG_LABELS\n")
|
|
59
64
|
|
|
60
65
|
return cls.with_env_expanded(labels=_labels)
|
|
@@ -36,6 +36,15 @@ from isolate.server import definitions, health
|
|
|
36
36
|
from isolate.server.health_server import HealthServicer
|
|
37
37
|
from isolate.server.interface import from_grpc, to_grpc
|
|
38
38
|
|
|
39
|
+
# Server-level logger for events that aren't tied to a RunTask (startup,
|
|
40
|
+
# signals, auth interceptor, lifecycle). Routes to stdout as JSON via
|
|
41
|
+
# IsolateLogger.log -> print(json.dumps(record)). Critical: this makes the
|
|
42
|
+
# very first line written to isolate-server.stdout a JSON record with a
|
|
43
|
+
# microsecond-precision logged_at, so Vector's default content-based file
|
|
44
|
+
# fingerprint (checksum, lines: 1) is unique per alloc instead of colliding
|
|
45
|
+
# on the constant "Started listening at 0.0.0.0:NNNNN" prefix.
|
|
46
|
+
_SERVER_LOGGER = IsolateLogger.from_env()
|
|
47
|
+
|
|
39
48
|
EMPTY_MESSAGE_INTERVAL = float(os.getenv("ISOLATE_EMPTY_MESSAGE_INTERVAL", "600"))
|
|
40
49
|
SKIP_EMPTY_LOGS = os.getenv("ISOLATE_SKIP_EMPTY_LOGS") == "1"
|
|
41
50
|
MAX_GRPC_WAIT_TIMEOUT = float(os.getenv("ISOLATE_MAX_GRPC_WAIT_TIMEOUT", "10.0"))
|
|
@@ -442,7 +451,12 @@ class IsolateServicer(definitions.IsolateServicer):
|
|
|
442
451
|
task.future = self._thread_pool.submit(self._run_task_in_background, task)
|
|
443
452
|
task_id = str(uuid.uuid4())
|
|
444
453
|
|
|
445
|
-
|
|
454
|
+
_SERVER_LOGGER.log(
|
|
455
|
+
LogLevel.INFO,
|
|
456
|
+
f"Submitted a task {task_id}",
|
|
457
|
+
source=LogSource.BRIDGE,
|
|
458
|
+
line_labels={},
|
|
459
|
+
)
|
|
446
460
|
|
|
447
461
|
self.background_tasks[task_id] = task
|
|
448
462
|
|
|
@@ -452,7 +466,12 @@ class IsolateServicer(definitions.IsolateServicer):
|
|
|
452
466
|
msg += f" error: {exc!r}"
|
|
453
467
|
else:
|
|
454
468
|
msg += f" result: {future.result()!r}"
|
|
455
|
-
|
|
469
|
+
_SERVER_LOGGER.log(
|
|
470
|
+
LogLevel.INFO,
|
|
471
|
+
msg,
|
|
472
|
+
source=LogSource.BRIDGE,
|
|
473
|
+
line_labels={},
|
|
474
|
+
)
|
|
456
475
|
self.background_tasks.pop(task_id, None)
|
|
457
476
|
|
|
458
477
|
task.future.add_done_callback(_callback)
|
|
@@ -518,7 +537,12 @@ class IsolateServicer(definitions.IsolateServicer):
|
|
|
518
537
|
) -> definitions.CancelResponse:
|
|
519
538
|
task_id = request.task_id
|
|
520
539
|
|
|
521
|
-
|
|
540
|
+
_SERVER_LOGGER.log(
|
|
541
|
+
LogLevel.INFO,
|
|
542
|
+
f"Canceling task {task_id}",
|
|
543
|
+
source=LogSource.BRIDGE,
|
|
544
|
+
line_labels={},
|
|
545
|
+
)
|
|
522
546
|
task = self.background_tasks.get(task_id)
|
|
523
547
|
if task is not None:
|
|
524
548
|
task.cancel()
|
|
@@ -527,14 +551,29 @@ class IsolateServicer(definitions.IsolateServicer):
|
|
|
527
551
|
|
|
528
552
|
def shutdown(self) -> None:
|
|
529
553
|
if self._shutting_down:
|
|
530
|
-
|
|
554
|
+
_SERVER_LOGGER.log(
|
|
555
|
+
LogLevel.INFO,
|
|
556
|
+
"Shutdown already in progress...",
|
|
557
|
+
source=LogSource.BRIDGE,
|
|
558
|
+
line_labels={},
|
|
559
|
+
)
|
|
531
560
|
return
|
|
532
561
|
|
|
533
562
|
self._shutting_down = True
|
|
534
563
|
task_count = len(self.background_tasks)
|
|
535
|
-
|
|
564
|
+
_SERVER_LOGGER.log(
|
|
565
|
+
LogLevel.INFO,
|
|
566
|
+
f"Shutting down, canceling {task_count} tasks...",
|
|
567
|
+
source=LogSource.BRIDGE,
|
|
568
|
+
line_labels={},
|
|
569
|
+
)
|
|
536
570
|
self.cancel_tasks()
|
|
537
|
-
|
|
571
|
+
_SERVER_LOGGER.log(
|
|
572
|
+
LogLevel.INFO,
|
|
573
|
+
"All tasks canceled.",
|
|
574
|
+
source=LogSource.BRIDGE,
|
|
575
|
+
line_labels={},
|
|
576
|
+
)
|
|
538
577
|
|
|
539
578
|
def watch_queue_until_completed(
|
|
540
579
|
self, queue: Queue, is_completed: Callable[[], bool]
|
|
@@ -703,11 +742,21 @@ class SingleTaskInterceptor(ServerBoundInterceptor):
|
|
|
703
742
|
def _wrapper(request: Any, context: grpc.ServicerContext) -> Any:
|
|
704
743
|
def termination() -> None:
|
|
705
744
|
if is_run:
|
|
706
|
-
|
|
745
|
+
_SERVER_LOGGER.log(
|
|
746
|
+
LogLevel.INFO,
|
|
747
|
+
"Stopping server since run is finished",
|
|
748
|
+
source=LogSource.BRIDGE,
|
|
749
|
+
line_labels={},
|
|
750
|
+
)
|
|
707
751
|
self.servicer.shutdown()
|
|
708
752
|
# Stop the server after the Run task is finished
|
|
709
753
|
self.server.stop(grace=0.1)
|
|
710
|
-
|
|
754
|
+
_SERVER_LOGGER.log(
|
|
755
|
+
LogLevel.INFO,
|
|
756
|
+
"Server stopped",
|
|
757
|
+
source=LogSource.BRIDGE,
|
|
758
|
+
line_labels={},
|
|
759
|
+
)
|
|
711
760
|
|
|
712
761
|
elif is_submit:
|
|
713
762
|
# Wait until the task_id is assigned
|
|
@@ -731,10 +780,20 @@ class SingleTaskInterceptor(ServerBoundInterceptor):
|
|
|
731
780
|
def _stop(*args):
|
|
732
781
|
# Small sleep to make sure the cancellation is processed
|
|
733
782
|
time.sleep(0.3)
|
|
734
|
-
|
|
783
|
+
_SERVER_LOGGER.log(
|
|
784
|
+
LogLevel.INFO,
|
|
785
|
+
"Stopping server since the task is finished",
|
|
786
|
+
source=LogSource.BRIDGE,
|
|
787
|
+
line_labels={},
|
|
788
|
+
)
|
|
735
789
|
self.servicer.shutdown()
|
|
736
790
|
self.server.stop(grace=0.1)
|
|
737
|
-
|
|
791
|
+
_SERVER_LOGGER.log(
|
|
792
|
+
LogLevel.INFO,
|
|
793
|
+
"Server stopped",
|
|
794
|
+
source=LogSource.BRIDGE,
|
|
795
|
+
line_labels={},
|
|
796
|
+
)
|
|
738
797
|
|
|
739
798
|
# Add a callback which will stop the server
|
|
740
799
|
# after the task is finished
|
|
@@ -771,7 +830,12 @@ class ControllerAuthInterceptor(ServerBoundInterceptor):
|
|
|
771
830
|
]
|
|
772
831
|
|
|
773
832
|
if handler_call_details.method in skipped_auth_methods:
|
|
774
|
-
|
|
833
|
+
_SERVER_LOGGER.log(
|
|
834
|
+
LogLevel.DEBUG,
|
|
835
|
+
f"Skipping authentication for {handler_call_details.method}",
|
|
836
|
+
source=LogSource.BRIDGE,
|
|
837
|
+
line_labels={},
|
|
838
|
+
)
|
|
775
839
|
# Let these requests pass through without authentication
|
|
776
840
|
return continuation(handler_call_details)
|
|
777
841
|
|
|
@@ -816,9 +880,12 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
816
880
|
# Set an interceptor to only accept requests with the correct auth key
|
|
817
881
|
interceptors.append(ControllerAuthInterceptor(controller_auth_key))
|
|
818
882
|
else:
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
"
|
|
883
|
+
_SERVER_LOGGER.log(
|
|
884
|
+
LogLevel.WARNING,
|
|
885
|
+
"ISOLATE_CONTROLLER_AUTH_KEY is not set, all requests will be "
|
|
886
|
+
"accepted without authentication.",
|
|
887
|
+
source=LogSource.BRIDGE,
|
|
888
|
+
line_labels={},
|
|
822
889
|
)
|
|
823
890
|
|
|
824
891
|
server = grpc.server(
|
|
@@ -839,6 +906,12 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
839
906
|
definitions.register_isolate(servicer, server)
|
|
840
907
|
health.register_health(HealthServicer(), server)
|
|
841
908
|
|
|
909
|
+
# Signal handlers intentionally use bare print(): IsolateLogger.log()
|
|
910
|
+
# calls datetime.now() and json.dumps() which allocate memory, and
|
|
911
|
+
# heap allocation in a Python signal handler is unsafe (can deadlock
|
|
912
|
+
# if the main thread is mid-allocation). These messages also fire at
|
|
913
|
+
# shutdown — well past the first stdout line — so they don't affect
|
|
914
|
+
# Vector's file fingerprint.
|
|
842
915
|
def handle_termination(*args):
|
|
843
916
|
print("Termination signal received, shutting down...")
|
|
844
917
|
servicer.shutdown()
|
|
@@ -853,11 +926,21 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
853
926
|
signal.signal(signal.SIGCHLD, handle_child_termination)
|
|
854
927
|
|
|
855
928
|
server.add_insecure_port(f"[::]:{options.port}")
|
|
856
|
-
|
|
929
|
+
_SERVER_LOGGER.log(
|
|
930
|
+
LogLevel.INFO,
|
|
931
|
+
f"Started listening at {options.host}:{options.port}",
|
|
932
|
+
source=LogSource.BRIDGE,
|
|
933
|
+
line_labels={},
|
|
934
|
+
)
|
|
857
935
|
|
|
858
936
|
server.start()
|
|
859
937
|
server.wait_for_termination()
|
|
860
|
-
|
|
938
|
+
_SERVER_LOGGER.log(
|
|
939
|
+
LogLevel.INFO,
|
|
940
|
+
"Server shut down",
|
|
941
|
+
source=LogSource.BRIDGE,
|
|
942
|
+
line_labels={},
|
|
943
|
+
)
|
|
861
944
|
|
|
862
945
|
|
|
863
946
|
if __name__ == "__main__":
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import os
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from unittest.mock import Mock, patch
|
|
4
|
+
from unittest.mock import Mock, call, patch
|
|
5
5
|
|
|
6
6
|
import grpc
|
|
7
7
|
import pytest
|
|
@@ -51,11 +51,23 @@ def test_abort_agent_logs_return_code_for_graceful_termination(tmp_path: Path) -
|
|
|
51
51
|
process.terminate.assert_called_once()
|
|
52
52
|
process.wait.assert_called_once()
|
|
53
53
|
process.kill.assert_not_called()
|
|
54
|
-
connection.log.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
assert connection.log.call_args_list == [
|
|
55
|
+
call(
|
|
56
|
+
"Terminating the agent process...",
|
|
57
|
+
level=LogLevel.INFO,
|
|
58
|
+
source=LogSource.BRIDGE,
|
|
59
|
+
),
|
|
60
|
+
call(
|
|
61
|
+
"Agent process shutdown gracefully",
|
|
62
|
+
level=LogLevel.INFO,
|
|
63
|
+
source=LogSource.BRIDGE,
|
|
64
|
+
),
|
|
65
|
+
call(
|
|
66
|
+
"Isolate agent gracefully terminated",
|
|
67
|
+
level=LogLevel.INFO,
|
|
68
|
+
source=LogSource.BRIDGE,
|
|
69
|
+
),
|
|
70
|
+
]
|
|
59
71
|
assert connection._process is None
|
|
60
72
|
|
|
61
73
|
|
|
@@ -73,11 +85,23 @@ def test_abort_agent_logs_return_code_after_kill_fallback(tmp_path: Path) -> Non
|
|
|
73
85
|
process.terminate.assert_called_once()
|
|
74
86
|
process.kill.assert_called_once()
|
|
75
87
|
process.wait.assert_called_once()
|
|
76
|
-
connection.log.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
88
|
+
assert connection.log.call_args_list == [
|
|
89
|
+
call(
|
|
90
|
+
"Terminating the agent process...",
|
|
91
|
+
level=LogLevel.INFO,
|
|
92
|
+
source=LogSource.BRIDGE,
|
|
93
|
+
),
|
|
94
|
+
call(
|
|
95
|
+
"Failed to shutdown the agent process gracefully: terminate failed",
|
|
96
|
+
level=LogLevel.ERROR,
|
|
97
|
+
source=LogSource.BRIDGE,
|
|
98
|
+
),
|
|
99
|
+
call(
|
|
100
|
+
"Isolate agent forcefully killed",
|
|
101
|
+
level=LogLevel.INFO,
|
|
102
|
+
source=LogSource.BRIDGE,
|
|
103
|
+
),
|
|
104
|
+
]
|
|
81
105
|
assert connection._process is None
|
|
82
106
|
|
|
83
107
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Regression tests for the pipe wiring used by `logged_io`.
|
|
2
|
+
|
|
3
|
+
Background: `_unblocked_pipe()` is consumed by `logged_io()`, where the
|
|
4
|
+
*read* end is drained from a reader thread (`_io_observer`) using
|
|
5
|
+
`select()` + `os.read()`. Only the read end needs to be non-blocking
|
|
6
|
+
(so the post-`select` read can never hang on a partial-line edge case).
|
|
7
|
+
|
|
8
|
+
Historically both ends were marked non-blocking. That makes the *write*
|
|
9
|
+
end raise `BlockingIOError` (errno EAGAIN) the moment the kernel pipe
|
|
10
|
+
buffer (~64 KB on Linux) fills up before the reader thread drains it —
|
|
11
|
+
which is exactly what happens when a worker emits a chunky traceback.
|
|
12
|
+
With a blocking write end, writers apply backpressure instead.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
import threading
|
|
21
|
+
|
|
22
|
+
import pytest
|
|
23
|
+
from isolate.backends.common import _unblocked_pipe, logged_io
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_unblocked_pipe_write_end_is_blocking():
|
|
27
|
+
"""The write end must be blocking so that worker stdout/stderr applies
|
|
28
|
+
backpressure instead of raising BlockingIOError when the pipe buffer fills."""
|
|
29
|
+
read_fd, write_fd = _unblocked_pipe()
|
|
30
|
+
try:
|
|
31
|
+
assert not os.get_blocking(read_fd), "read end must be non-blocking"
|
|
32
|
+
assert os.get_blocking(write_fd), (
|
|
33
|
+
"write end must be blocking so the worker gets backpressure "
|
|
34
|
+
"instead of BlockingIOError when the pipe buffer fills up"
|
|
35
|
+
)
|
|
36
|
+
finally:
|
|
37
|
+
os.close(read_fd)
|
|
38
|
+
os.close(write_fd)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_writing_more_than_pipe_buffer_does_not_raise_blocking_io_error():
|
|
42
|
+
"""Direct reproducer: write more than the kernel pipe buffer (64 KB on
|
|
43
|
+
Linux) through a Python BufferedWriter on top of the pipe's write end,
|
|
44
|
+
while a reader thread drains it slowly. With a non-blocking write end
|
|
45
|
+
this raises `BlockingIOError`; with a blocking write end the writer
|
|
46
|
+
transparently waits for the reader to make room.
|
|
47
|
+
"""
|
|
48
|
+
read_fd, write_fd = _unblocked_pipe()
|
|
49
|
+
drained = bytearray()
|
|
50
|
+
drain_done = threading.Event()
|
|
51
|
+
|
|
52
|
+
def drain():
|
|
53
|
+
# Slow reader: read in small chunks with a `select` loop, mimicking
|
|
54
|
+
# `_io_observer`'s pattern but slower so the pipe buffer fills up.
|
|
55
|
+
import select as _select
|
|
56
|
+
|
|
57
|
+
while not drain_done.is_set():
|
|
58
|
+
ready, _, _ = _select.select([read_fd], [], [], 0.05)
|
|
59
|
+
if read_fd in ready:
|
|
60
|
+
try:
|
|
61
|
+
chunk = os.read(read_fd, 4096)
|
|
62
|
+
except BlockingIOError:
|
|
63
|
+
continue
|
|
64
|
+
if not chunk:
|
|
65
|
+
return
|
|
66
|
+
drained.extend(chunk)
|
|
67
|
+
|
|
68
|
+
reader = threading.Thread(target=drain)
|
|
69
|
+
reader.start()
|
|
70
|
+
try:
|
|
71
|
+
# 512 KB is well past the 64 KB pipe buffer on Linux.
|
|
72
|
+
payload = b"x" * (512 * 1024)
|
|
73
|
+
with open(write_fd, "wb", closefd=False) as stream:
|
|
74
|
+
stream.write(payload)
|
|
75
|
+
stream.flush()
|
|
76
|
+
# Give the reader a moment to catch up.
|
|
77
|
+
deadline_loops = 50
|
|
78
|
+
while len(drained) < len(payload) and deadline_loops > 0:
|
|
79
|
+
deadline_loops -= 1
|
|
80
|
+
drain_done.wait(0.05)
|
|
81
|
+
finally:
|
|
82
|
+
drain_done.set()
|
|
83
|
+
os.close(write_fd)
|
|
84
|
+
reader.join(timeout=2)
|
|
85
|
+
os.close(read_fd)
|
|
86
|
+
|
|
87
|
+
assert len(drained) == len(
|
|
88
|
+
payload
|
|
89
|
+
), f"reader only drained {len(drained)} of {len(payload)} bytes"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@pytest.mark.skipif(sys.platform == "win32", reason="pipes / pass_fds are POSIX-y")
|
|
93
|
+
def test_logged_io_subprocess_emitting_large_output_does_not_crash():
|
|
94
|
+
"""End-to-end reproducer mirroring how `logged_io` is used in
|
|
95
|
+
`isolate.connections._local._base`: a child process writes a chunky
|
|
96
|
+
payload (~256 KB) to stdout. With a non-blocking write end the child's
|
|
97
|
+
Python runtime raises `BlockingIOError` when flushing stdout; with
|
|
98
|
+
backpressure the child completes cleanly and the parent receives
|
|
99
|
+
every line."""
|
|
100
|
+
|
|
101
|
+
captured: list[str] = []
|
|
102
|
+
lock = threading.Lock()
|
|
103
|
+
|
|
104
|
+
def record(line: str) -> None:
|
|
105
|
+
with lock:
|
|
106
|
+
captured.append(line)
|
|
107
|
+
|
|
108
|
+
n_lines = 4000
|
|
109
|
+
line_template = "trace-{idx:08d}-" + ("y" * 50)
|
|
110
|
+
child_script = (
|
|
111
|
+
"import sys\n"
|
|
112
|
+
f"for i in range({n_lines}):\n"
|
|
113
|
+
f" sys.stdout.write({line_template!r}.format(idx=i) + '\\n')\n"
|
|
114
|
+
"sys.stdout.flush()\n"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
with logged_io(record) as (stdout, stderr, log_fd):
|
|
118
|
+
proc = subprocess.run(
|
|
119
|
+
[sys.executable, "-u", "-c", child_script],
|
|
120
|
+
stdout=stdout,
|
|
121
|
+
stderr=stderr,
|
|
122
|
+
pass_fds=(log_fd,),
|
|
123
|
+
text=True,
|
|
124
|
+
check=False,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
assert proc.returncode == 0, (
|
|
128
|
+
f"child exited with {proc.returncode}; with a non-blocking write end "
|
|
129
|
+
"Python's stdout buffer raises BlockingIOError on large flushes"
|
|
130
|
+
)
|
|
131
|
+
# All `n_lines` should have been captured by the observer thread.
|
|
132
|
+
assert len(captured) == n_lines, f"expected {n_lines} lines, got {len(captured)}"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{isolate-0.26.4 → isolate-0.26.6}/src/isolate/connections/grpc/definitions/agent_pb2_grpc.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{isolate-0.26.4 → isolate-0.26.6}/src/isolate/connections/grpc/definitions/common_pb2_grpc.py
RENAMED
|
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
|
|
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
|