boxlite 0.5.7__cp312-cp312-macosx_14_0_arm64.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.
Potentially problematic release.
This version of boxlite might be problematic. Click here for more details.
- boxlite/__init__.py +123 -0
- boxlite/boxlite.cpython-312-darwin.so +0 -0
- boxlite/browserbox.py +142 -0
- boxlite/codebox.py +120 -0
- boxlite/computerbox.py +302 -0
- boxlite/constants.py +25 -0
- boxlite/errors.py +44 -0
- boxlite/exec.py +27 -0
- boxlite/interactivebox.py +287 -0
- boxlite/runtime/boxlite-guest +0 -0
- boxlite/runtime/boxlite-shim +0 -0
- boxlite/runtime/debugfs +0 -0
- boxlite/runtime/libgvproxy.dylib +0 -0
- boxlite/runtime/libkrun.1.16.0.dylib +0 -0
- boxlite/runtime/libkrunfw.5.dylib +0 -0
- boxlite/runtime/mke2fs +0 -0
- boxlite/simplebox.py +256 -0
- boxlite/sync_api/__init__.py +65 -0
- boxlite/sync_api/_box.py +133 -0
- boxlite/sync_api/_boxlite.py +377 -0
- boxlite/sync_api/_codebox.py +145 -0
- boxlite/sync_api/_execution.py +203 -0
- boxlite/sync_api/_simplebox.py +180 -0
- boxlite/sync_api/_sync_base.py +137 -0
- boxlite-0.5.7.dist-info/METADATA +845 -0
- boxlite-0.5.7.dist-info/RECORD +27 -0
- boxlite-0.5.7.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SyncCodeBox - Synchronous wrapper for CodeBox.
|
|
3
|
+
|
|
4
|
+
Provides a synchronous API for Python code execution using greenlet fiber switching.
|
|
5
|
+
API mirrors async CodeBox exactly.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING, Optional
|
|
9
|
+
|
|
10
|
+
from ._simplebox import SyncSimpleBox
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ._boxlite import SyncBoxlite
|
|
14
|
+
|
|
15
|
+
__all__ = ["SyncCodeBox"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SyncCodeBox(SyncSimpleBox):
|
|
19
|
+
"""
|
|
20
|
+
Synchronous wrapper for CodeBox.
|
|
21
|
+
|
|
22
|
+
Provides synchronous methods for executing Python code in a secure container.
|
|
23
|
+
Built on top of SyncSimpleBox with Python-specific convenience methods.
|
|
24
|
+
API mirrors async CodeBox exactly.
|
|
25
|
+
|
|
26
|
+
Usage (standalone - recommended):
|
|
27
|
+
with SyncCodeBox() as box:
|
|
28
|
+
result = box.run("print('Hello, World!')")
|
|
29
|
+
print(result) # Hello, World!
|
|
30
|
+
|
|
31
|
+
Usage (with explicit runtime):
|
|
32
|
+
with SyncBoxlite.default() as runtime:
|
|
33
|
+
with SyncCodeBox(runtime=runtime) as box:
|
|
34
|
+
result = box.run("print('Hello!')")
|
|
35
|
+
print(result)
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
image: str = "python:slim",
|
|
41
|
+
memory_mib: Optional[int] = None,
|
|
42
|
+
cpus: Optional[int] = None,
|
|
43
|
+
runtime: Optional["SyncBoxlite"] = None,
|
|
44
|
+
name: Optional[str] = None,
|
|
45
|
+
auto_remove: bool = True,
|
|
46
|
+
**kwargs,
|
|
47
|
+
):
|
|
48
|
+
"""
|
|
49
|
+
Create a SyncCodeBox.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
image: Python container image (default: "python:slim")
|
|
53
|
+
memory_mib: Memory limit in MiB (default: system default)
|
|
54
|
+
cpus: Number of CPU cores (default: system default)
|
|
55
|
+
runtime: Optional SyncBoxlite runtime. If None, creates default runtime.
|
|
56
|
+
name: Optional unique name for the box
|
|
57
|
+
auto_remove: Remove box when stopped (default: True)
|
|
58
|
+
**kwargs: Additional BoxOptions parameters
|
|
59
|
+
"""
|
|
60
|
+
super().__init__(
|
|
61
|
+
image=image,
|
|
62
|
+
memory_mib=memory_mib,
|
|
63
|
+
cpus=cpus,
|
|
64
|
+
runtime=runtime,
|
|
65
|
+
name=name,
|
|
66
|
+
auto_remove=auto_remove,
|
|
67
|
+
**kwargs,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def run(self, code: str, timeout: Optional[int] = None) -> str:
|
|
71
|
+
"""
|
|
72
|
+
Execute Python code synchronously.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
code: Python code to execute
|
|
76
|
+
timeout: Execution timeout in seconds (not yet implemented)
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Combined stdout and stderr output
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
with SyncCodeBox() as box:
|
|
83
|
+
result = box.run("print('Hello!')")
|
|
84
|
+
print(result) # Hello!
|
|
85
|
+
|
|
86
|
+
# Multi-line code
|
|
87
|
+
result = box.run('''
|
|
88
|
+
import sys
|
|
89
|
+
print(f"Python {sys.version}")
|
|
90
|
+
''')
|
|
91
|
+
"""
|
|
92
|
+
result = self.exec("/usr/local/bin/python", "-c", code)
|
|
93
|
+
return result.stdout + result.stderr
|
|
94
|
+
|
|
95
|
+
def install_package(self, package: str) -> str:
|
|
96
|
+
"""
|
|
97
|
+
Install a Python package using pip.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
package: Package name (e.g., "requests", "numpy==1.24.0")
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Installation output
|
|
104
|
+
|
|
105
|
+
Example:
|
|
106
|
+
box.install_package("requests")
|
|
107
|
+
result = box.run("import requests; print(requests.__version__)")
|
|
108
|
+
"""
|
|
109
|
+
result = self.exec("pip", "install", package)
|
|
110
|
+
return result.stdout + result.stderr
|
|
111
|
+
|
|
112
|
+
def install_packages(self, *packages: str) -> str:
|
|
113
|
+
"""
|
|
114
|
+
Install multiple Python packages.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
*packages: Package names to install
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Installation output
|
|
121
|
+
|
|
122
|
+
Example:
|
|
123
|
+
box.install_packages("requests", "numpy", "pandas")
|
|
124
|
+
"""
|
|
125
|
+
result = self.exec("pip", "install", *packages)
|
|
126
|
+
return result.stdout + result.stderr
|
|
127
|
+
|
|
128
|
+
def run_script(self, script_path: str) -> str:
|
|
129
|
+
"""
|
|
130
|
+
Execute a Python script file.
|
|
131
|
+
|
|
132
|
+
Reads the script from the host filesystem and executes it in the box.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
script_path: Path to the Python script on the host
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Script output (stdout + stderr)
|
|
139
|
+
|
|
140
|
+
Example:
|
|
141
|
+
result = box.run_script("./my_script.py")
|
|
142
|
+
"""
|
|
143
|
+
with open(script_path, "r") as f:
|
|
144
|
+
code = f.read()
|
|
145
|
+
return self.run(code)
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SyncExecution - Synchronous wrapper for Execution.
|
|
3
|
+
|
|
4
|
+
Mirrors the native Execution API exactly, but with synchronous methods.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING, Optional
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from ._boxlite import SyncBoxlite
|
|
11
|
+
from ..boxlite import Execution
|
|
12
|
+
|
|
13
|
+
__all__ = ["SyncExecution", "SyncExecStdout", "SyncExecStderr"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SyncExecStdout:
|
|
17
|
+
"""
|
|
18
|
+
Synchronous iterator for execution stdout.
|
|
19
|
+
|
|
20
|
+
Mirrors ExecStdout but uses regular iteration instead of async iteration.
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
stdout = execution.stdout()
|
|
24
|
+
for line in stdout:
|
|
25
|
+
print(line)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, ctx: "SyncBoxlite", async_stdout) -> None:
|
|
29
|
+
self._ctx = ctx
|
|
30
|
+
self._async_stdout = async_stdout
|
|
31
|
+
self._async_iter = None
|
|
32
|
+
|
|
33
|
+
from ._sync_base import SyncBase
|
|
34
|
+
|
|
35
|
+
self._sync_helper = SyncBase(async_stdout, ctx.loop, ctx.dispatcher_fiber)
|
|
36
|
+
|
|
37
|
+
def _sync(self, coro):
|
|
38
|
+
"""Run async operation synchronously."""
|
|
39
|
+
return self._sync_helper._sync(coro)
|
|
40
|
+
|
|
41
|
+
def __iter__(self) -> "SyncExecStdout":
|
|
42
|
+
"""Start iteration."""
|
|
43
|
+
self._async_iter = self._async_stdout.__aiter__()
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
def __next__(self) -> str:
|
|
47
|
+
"""Get next line from stdout."""
|
|
48
|
+
if self._async_iter is None:
|
|
49
|
+
self._async_iter = self._async_stdout.__aiter__()
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
line = self._sync(self._async_iter.__anext__())
|
|
53
|
+
# Decode bytes to string if needed
|
|
54
|
+
if isinstance(line, bytes):
|
|
55
|
+
return line.decode("utf-8", errors="replace")
|
|
56
|
+
return line
|
|
57
|
+
except StopAsyncIteration:
|
|
58
|
+
raise StopIteration
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SyncExecStderr:
|
|
62
|
+
"""
|
|
63
|
+
Synchronous iterator for execution stderr.
|
|
64
|
+
|
|
65
|
+
Mirrors ExecStderr but uses regular iteration instead of async iteration.
|
|
66
|
+
|
|
67
|
+
Usage:
|
|
68
|
+
stderr = execution.stderr()
|
|
69
|
+
for line in stderr:
|
|
70
|
+
print(line)
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(self, ctx: "SyncBoxlite", async_stderr) -> None:
|
|
74
|
+
self._ctx = ctx
|
|
75
|
+
self._async_stderr = async_stderr
|
|
76
|
+
self._async_iter = None
|
|
77
|
+
|
|
78
|
+
from ._sync_base import SyncBase
|
|
79
|
+
|
|
80
|
+
self._sync_helper = SyncBase(async_stderr, ctx.loop, ctx.dispatcher_fiber)
|
|
81
|
+
|
|
82
|
+
def _sync(self, coro):
|
|
83
|
+
"""Run async operation synchronously."""
|
|
84
|
+
return self._sync_helper._sync(coro)
|
|
85
|
+
|
|
86
|
+
def __iter__(self) -> "SyncExecStderr":
|
|
87
|
+
"""Start iteration."""
|
|
88
|
+
self._async_iter = self._async_stderr.__aiter__()
|
|
89
|
+
return self
|
|
90
|
+
|
|
91
|
+
def __next__(self) -> str:
|
|
92
|
+
"""Get next line from stderr."""
|
|
93
|
+
if self._async_iter is None:
|
|
94
|
+
self._async_iter = self._async_stderr.__aiter__()
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
line = self._sync(self._async_iter.__anext__())
|
|
98
|
+
# Decode bytes to string if needed
|
|
99
|
+
if isinstance(line, bytes):
|
|
100
|
+
return line.decode("utf-8", errors="replace")
|
|
101
|
+
return line
|
|
102
|
+
except StopAsyncIteration:
|
|
103
|
+
raise StopIteration
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class SyncExecution:
|
|
107
|
+
"""
|
|
108
|
+
Synchronous wrapper for Execution.
|
|
109
|
+
|
|
110
|
+
Provides the same API as the native Execution class, but with synchronous methods.
|
|
111
|
+
stdout() and stderr() return sync iterables instead of async iterables.
|
|
112
|
+
|
|
113
|
+
Usage:
|
|
114
|
+
execution = box.exec("echo", ["Hello"])
|
|
115
|
+
|
|
116
|
+
# Stream stdout
|
|
117
|
+
for line in execution.stdout():
|
|
118
|
+
print(f"stdout: {line}")
|
|
119
|
+
|
|
120
|
+
# Stream stderr
|
|
121
|
+
for line in execution.stderr():
|
|
122
|
+
print(f"stderr: {line}")
|
|
123
|
+
|
|
124
|
+
# Wait for completion
|
|
125
|
+
result = execution.wait()
|
|
126
|
+
print(f"Exit code: {result.exit_code}")
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __init__(
|
|
130
|
+
self,
|
|
131
|
+
ctx: "SyncBoxlite",
|
|
132
|
+
execution: "Execution",
|
|
133
|
+
) -> None:
|
|
134
|
+
"""
|
|
135
|
+
Create a SyncExecution wrapper.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
ctx: The SyncBoxlite providing event loop and dispatcher
|
|
139
|
+
execution: The native Execution object to wrap
|
|
140
|
+
"""
|
|
141
|
+
from ._sync_base import SyncBase
|
|
142
|
+
|
|
143
|
+
self._execution = execution
|
|
144
|
+
self._ctx = ctx
|
|
145
|
+
self._sync_helper = SyncBase(execution, ctx.loop, ctx.dispatcher_fiber)
|
|
146
|
+
|
|
147
|
+
def _sync(self, coro):
|
|
148
|
+
"""Run async operation synchronously."""
|
|
149
|
+
return self._sync_helper._sync(coro)
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def id(self) -> str:
|
|
153
|
+
"""Get the execution ID."""
|
|
154
|
+
return self._execution.id
|
|
155
|
+
|
|
156
|
+
def stdout(self) -> Optional[SyncExecStdout]:
|
|
157
|
+
"""
|
|
158
|
+
Get synchronous stdout iterator.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
SyncExecStdout iterator, or None if stdout is not available.
|
|
162
|
+
|
|
163
|
+
Usage:
|
|
164
|
+
stdout = execution.stdout()
|
|
165
|
+
if stdout:
|
|
166
|
+
for line in stdout:
|
|
167
|
+
print(line)
|
|
168
|
+
"""
|
|
169
|
+
async_stdout = self._execution.stdout()
|
|
170
|
+
if async_stdout is None:
|
|
171
|
+
return None
|
|
172
|
+
return SyncExecStdout(self._ctx, async_stdout)
|
|
173
|
+
|
|
174
|
+
def stderr(self) -> Optional[SyncExecStderr]:
|
|
175
|
+
"""
|
|
176
|
+
Get synchronous stderr iterator.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
SyncExecStderr iterator, or None if stderr is not available.
|
|
180
|
+
|
|
181
|
+
Usage:
|
|
182
|
+
stderr = execution.stderr()
|
|
183
|
+
if stderr:
|
|
184
|
+
for line in stderr:
|
|
185
|
+
print(line)
|
|
186
|
+
"""
|
|
187
|
+
async_stderr = self._execution.stderr()
|
|
188
|
+
if async_stderr is None:
|
|
189
|
+
return None
|
|
190
|
+
return SyncExecStderr(self._ctx, async_stderr)
|
|
191
|
+
|
|
192
|
+
def wait(self):
|
|
193
|
+
"""
|
|
194
|
+
Wait for execution to complete.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
ExecResult with exit_code and other completion info.
|
|
198
|
+
"""
|
|
199
|
+
return self._sync(self._execution.wait())
|
|
200
|
+
|
|
201
|
+
def kill(self) -> None:
|
|
202
|
+
"""Kill the running execution."""
|
|
203
|
+
self._sync(self._execution.kill())
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SyncSimpleBox - Synchronous wrapper for SimpleBox.
|
|
3
|
+
|
|
4
|
+
Provides a synchronous API for box operations.
|
|
5
|
+
API mirrors async SimpleBox exactly.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import TYPE_CHECKING, Dict, Optional
|
|
9
|
+
|
|
10
|
+
from ..exec import ExecResult
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ._boxlite import SyncBoxlite
|
|
14
|
+
from ._box import SyncBox
|
|
15
|
+
|
|
16
|
+
__all__ = ["SyncSimpleBox"]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SyncSimpleBox:
|
|
20
|
+
"""
|
|
21
|
+
Synchronous wrapper for SimpleBox.
|
|
22
|
+
|
|
23
|
+
Provides synchronous methods for executing commands in a BoxLite container.
|
|
24
|
+
Uses SyncBox internally which handles async bridging via greenlet.
|
|
25
|
+
API mirrors async SimpleBox exactly.
|
|
26
|
+
|
|
27
|
+
Usage (standalone - recommended):
|
|
28
|
+
with SyncSimpleBox(image="python:slim") as box:
|
|
29
|
+
result = box.exec("ls", "-la")
|
|
30
|
+
print(result.stdout)
|
|
31
|
+
|
|
32
|
+
Usage (with explicit runtime):
|
|
33
|
+
with SyncBoxlite.default() as runtime:
|
|
34
|
+
with SyncSimpleBox(image="python:slim", runtime=runtime) as box:
|
|
35
|
+
result = box.exec("ls", "-la")
|
|
36
|
+
print(result.stdout)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
image: str,
|
|
42
|
+
memory_mib: Optional[int] = None,
|
|
43
|
+
cpus: Optional[int] = None,
|
|
44
|
+
runtime: Optional["SyncBoxlite"] = None,
|
|
45
|
+
name: Optional[str] = None,
|
|
46
|
+
auto_remove: bool = True,
|
|
47
|
+
**kwargs,
|
|
48
|
+
):
|
|
49
|
+
"""
|
|
50
|
+
Create a SyncSimpleBox.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
image: Container image to use (e.g., "python:slim", "ubuntu:latest")
|
|
54
|
+
memory_mib: Memory limit in MiB (default: system default)
|
|
55
|
+
cpus: Number of CPU cores (default: system default)
|
|
56
|
+
runtime: Optional SyncBoxlite runtime. If None, creates default runtime.
|
|
57
|
+
name: Optional unique name for the box
|
|
58
|
+
auto_remove: Remove box when stopped (default: True)
|
|
59
|
+
**kwargs: Additional BoxOptions parameters
|
|
60
|
+
"""
|
|
61
|
+
from ._boxlite import SyncBoxlite
|
|
62
|
+
from ..boxlite import BoxOptions
|
|
63
|
+
|
|
64
|
+
# Handle optional runtime
|
|
65
|
+
if runtime is None:
|
|
66
|
+
runtime = SyncBoxlite.default()
|
|
67
|
+
self._owns_runtime = True
|
|
68
|
+
else:
|
|
69
|
+
self._owns_runtime = False
|
|
70
|
+
|
|
71
|
+
self._runtime = runtime
|
|
72
|
+
|
|
73
|
+
# Create box options
|
|
74
|
+
self._box_opts = BoxOptions(
|
|
75
|
+
image=image,
|
|
76
|
+
cpus=cpus,
|
|
77
|
+
memory_mib=memory_mib,
|
|
78
|
+
auto_remove=auto_remove,
|
|
79
|
+
**kwargs,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Store for lazy creation in __enter__
|
|
83
|
+
self._name = name
|
|
84
|
+
self._box: Optional["SyncBox"] = None
|
|
85
|
+
|
|
86
|
+
def __enter__(self) -> "SyncSimpleBox":
|
|
87
|
+
"""Enter context - starts runtime if owned, then starts the box."""
|
|
88
|
+
# Start runtime if we own it
|
|
89
|
+
if self._owns_runtime:
|
|
90
|
+
self._runtime.start()
|
|
91
|
+
|
|
92
|
+
# Create box via runtime - returns SyncBox!
|
|
93
|
+
self._box = self._runtime.create(self._box_opts, name=self._name)
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
97
|
+
"""Exit context - stops the box, then stops runtime if owned."""
|
|
98
|
+
# Stop the box (SyncBox.stop() is already sync)
|
|
99
|
+
if self._box is not None:
|
|
100
|
+
self._box.stop()
|
|
101
|
+
|
|
102
|
+
# Stop runtime if we own it
|
|
103
|
+
if self._owns_runtime:
|
|
104
|
+
self._runtime.stop()
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def id(self) -> str:
|
|
108
|
+
"""Get the box ID."""
|
|
109
|
+
return self._box.id
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def name(self) -> Optional[str]:
|
|
113
|
+
"""Get the box name (if set)."""
|
|
114
|
+
return self._box.name
|
|
115
|
+
|
|
116
|
+
def info(self):
|
|
117
|
+
"""Get box information."""
|
|
118
|
+
return self._box.info()
|
|
119
|
+
|
|
120
|
+
def exec(
|
|
121
|
+
self,
|
|
122
|
+
cmd: str,
|
|
123
|
+
*args: str,
|
|
124
|
+
env: Optional[Dict[str, str]] = None,
|
|
125
|
+
) -> ExecResult:
|
|
126
|
+
"""
|
|
127
|
+
Execute a command in the box synchronously.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
cmd: Command to run (e.g., "ls", "python")
|
|
131
|
+
*args: Command arguments (e.g., "-l", "-a")
|
|
132
|
+
env: Environment variables as dict
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
ExecResult with exit_code, stdout, and stderr
|
|
136
|
+
|
|
137
|
+
Example:
|
|
138
|
+
result = box.exec("ls", "-la")
|
|
139
|
+
print(f"Exit code: {result.exit_code}")
|
|
140
|
+
print(f"Output: {result.stdout}")
|
|
141
|
+
"""
|
|
142
|
+
# Convert args to list format expected by SyncBox
|
|
143
|
+
arg_list = list(args) if args else None
|
|
144
|
+
env_list = list(env.items()) if env else None
|
|
145
|
+
|
|
146
|
+
# SyncBox.exec() returns SyncExecution - already sync!
|
|
147
|
+
execution = self._box.exec(cmd, arg_list, env_list)
|
|
148
|
+
|
|
149
|
+
# Collect stdout (sync iteration)
|
|
150
|
+
stdout_lines = []
|
|
151
|
+
for line in execution.stdout():
|
|
152
|
+
if isinstance(line, bytes):
|
|
153
|
+
stdout_lines.append(line.decode("utf-8", errors="replace"))
|
|
154
|
+
else:
|
|
155
|
+
stdout_lines.append(line)
|
|
156
|
+
|
|
157
|
+
# Collect stderr (sync iteration)
|
|
158
|
+
stderr_lines = []
|
|
159
|
+
for line in execution.stderr():
|
|
160
|
+
if isinstance(line, bytes):
|
|
161
|
+
stderr_lines.append(line.decode("utf-8", errors="replace"))
|
|
162
|
+
else:
|
|
163
|
+
stderr_lines.append(line)
|
|
164
|
+
|
|
165
|
+
# Wait for completion (sync)
|
|
166
|
+
result = execution.wait()
|
|
167
|
+
|
|
168
|
+
return ExecResult(
|
|
169
|
+
exit_code=result.exit_code,
|
|
170
|
+
stdout="".join(stdout_lines),
|
|
171
|
+
stderr="".join(stderr_lines),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def stop(self) -> None:
|
|
175
|
+
"""Stop the box (preserves state for restart)."""
|
|
176
|
+
self._box.stop()
|
|
177
|
+
|
|
178
|
+
def metrics(self):
|
|
179
|
+
"""Get box metrics (CPU, memory usage)."""
|
|
180
|
+
return self._box.metrics()
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base class for sync wrappers - provides _sync() method.
|
|
3
|
+
|
|
4
|
+
This module contains the core bridging logic that allows sync code to
|
|
5
|
+
execute async operations using greenlet fiber switching.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import inspect
|
|
10
|
+
import traceback
|
|
11
|
+
from typing import Any, Awaitable, Coroutine, TypeVar, Union
|
|
12
|
+
|
|
13
|
+
from greenlet import greenlet
|
|
14
|
+
|
|
15
|
+
__all__ = ["SyncBase", "SyncContextManager"]
|
|
16
|
+
|
|
17
|
+
T = TypeVar("T")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SyncBase:
|
|
21
|
+
"""
|
|
22
|
+
Base class for all sync wrapper objects.
|
|
23
|
+
|
|
24
|
+
Provides the _sync() method that bridges async to sync using greenlet
|
|
25
|
+
fiber switching. This is the core mechanism that allows synchronous
|
|
26
|
+
code to execute asynchronous operations.
|
|
27
|
+
|
|
28
|
+
How it works:
|
|
29
|
+
1. User calls a sync method (e.g., box.run_command())
|
|
30
|
+
2. Sync method calls _sync(async_coro)
|
|
31
|
+
3. _sync() creates an asyncio task and switches to dispatcher fiber
|
|
32
|
+
4. Dispatcher fiber runs event loop, processes the task
|
|
33
|
+
5. When task completes, callback switches back to user fiber
|
|
34
|
+
6. _sync() returns the task result
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
impl_obj: Any,
|
|
40
|
+
loop: asyncio.AbstractEventLoop,
|
|
41
|
+
dispatcher_fiber: greenlet,
|
|
42
|
+
) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Initialize SyncBase.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
impl_obj: The underlying async implementation object
|
|
48
|
+
loop: The asyncio event loop (managed by dispatcher)
|
|
49
|
+
dispatcher_fiber: The greenlet fiber running the event loop
|
|
50
|
+
"""
|
|
51
|
+
self._impl = impl_obj
|
|
52
|
+
self._loop = loop
|
|
53
|
+
self._dispatcher_fiber = dispatcher_fiber
|
|
54
|
+
|
|
55
|
+
def _sync(
|
|
56
|
+
self,
|
|
57
|
+
coro: Union[Coroutine[Any, Any, T], Awaitable[T]],
|
|
58
|
+
) -> T:
|
|
59
|
+
"""
|
|
60
|
+
Run async coroutine synchronously using greenlet fiber switching.
|
|
61
|
+
|
|
62
|
+
This is the core bridging method that enables sync-to-async conversion.
|
|
63
|
+
|
|
64
|
+
The method:
|
|
65
|
+
1. Creates an asyncio task from the coroutine
|
|
66
|
+
2. Registers a callback to switch back when task completes
|
|
67
|
+
3. Switches to dispatcher fiber to let event loop run
|
|
68
|
+
4. Returns the task result (or raises exception)
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
coro: The async coroutine to execute
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
The result of the coroutine
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
RuntimeError: If event loop is closed
|
|
78
|
+
Any exception raised by the coroutine
|
|
79
|
+
"""
|
|
80
|
+
__tracebackhide__ = True # Hide from pytest tracebacks
|
|
81
|
+
|
|
82
|
+
# Guard: event loop must be open
|
|
83
|
+
if self._loop.is_closed():
|
|
84
|
+
if hasattr(coro, "close"):
|
|
85
|
+
coro.close()
|
|
86
|
+
raise RuntimeError("Event loop is closed! Is BoxLite stopped?")
|
|
87
|
+
|
|
88
|
+
# 1. Get current fiber (user fiber)
|
|
89
|
+
g_self = greenlet.getcurrent()
|
|
90
|
+
|
|
91
|
+
# 2. Create async task from coroutine/future
|
|
92
|
+
# Note: PyO3's async methods return Future objects (not native coroutines),
|
|
93
|
+
# so we use ensure_future() which handles both coroutines and futures.
|
|
94
|
+
task: asyncio.Task = asyncio.ensure_future(coro, loop=self._loop)
|
|
95
|
+
|
|
96
|
+
# 3. Attach debug info for better stack traces
|
|
97
|
+
setattr(task, "__boxlite_stack__", inspect.stack(0))
|
|
98
|
+
setattr(task, "__boxlite_stack_trace__", traceback.extract_stack(limit=10))
|
|
99
|
+
|
|
100
|
+
# 4. When task completes, switch back to us
|
|
101
|
+
task.add_done_callback(lambda _: g_self.switch())
|
|
102
|
+
|
|
103
|
+
# 5. THE CORE LOOP: Keep switching to dispatcher until done
|
|
104
|
+
while not task.done():
|
|
105
|
+
self._dispatcher_fiber.switch()
|
|
106
|
+
# ^^^^^ Control goes to dispatcher fiber
|
|
107
|
+
# Dispatcher runs event loop, processes our task
|
|
108
|
+
# When task completes, callback fires g_self.switch()
|
|
109
|
+
# Control returns HERE
|
|
110
|
+
|
|
111
|
+
# 6. Return result (or raise exception)
|
|
112
|
+
return task.result()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class SyncContextManager(SyncBase):
|
|
116
|
+
"""
|
|
117
|
+
SyncBase with context manager support.
|
|
118
|
+
|
|
119
|
+
Provides __enter__ and __exit__ methods for use with 'with' statement.
|
|
120
|
+
Subclasses should override close() for cleanup logic.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def __enter__(self) -> "SyncContextManager":
|
|
124
|
+
"""Enter context manager."""
|
|
125
|
+
return self
|
|
126
|
+
|
|
127
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
128
|
+
"""Exit context manager - calls close()."""
|
|
129
|
+
self.close()
|
|
130
|
+
|
|
131
|
+
def close(self) -> None:
|
|
132
|
+
"""
|
|
133
|
+
Close and cleanup resources.
|
|
134
|
+
|
|
135
|
+
Override in subclasses to implement cleanup logic.
|
|
136
|
+
"""
|
|
137
|
+
pass
|