grasp-sdk 0.1.8__py3-none-any.whl → 0.2.0a1__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.
Potentially problematic release.
This version of grasp-sdk might be problematic. Click here for more details.
- examples/example_async_context.py +122 -0
- examples/example_binary_file_support.py +127 -0
- examples/example_grasp_usage.py +82 -0
- examples/example_readfile_usage.py +136 -0
- examples/grasp_terminal.py +110 -0
- examples/grasp_usage.py +111 -0
- examples/test_async_context.py +64 -0
- examples/test_get_replay_screenshots.py +90 -0
- examples/test_grasp_classes.py +80 -0
- examples/test_python_script.py +160 -0
- examples/test_removed_methods.py +80 -0
- examples/test_shutdown_deprecation.py +62 -0
- examples/test_terminal_updates.py +196 -0
- grasp_sdk/__init__.py +131 -239
- grasp_sdk/grasp/__init__.py +26 -0
- grasp_sdk/grasp/browser.py +69 -0
- grasp_sdk/grasp/index.py +122 -0
- grasp_sdk/grasp/server.py +250 -0
- grasp_sdk/grasp/session.py +108 -0
- grasp_sdk/grasp/terminal.py +32 -0
- grasp_sdk/grasp/utils.py +90 -0
- grasp_sdk/models/__init__.py +1 -1
- grasp_sdk/services/__init__.py +8 -1
- grasp_sdk/services/browser.py +92 -63
- grasp_sdk/services/filesystem.py +94 -0
- grasp_sdk/services/sandbox.py +391 -20
- grasp_sdk/services/terminal.py +177 -0
- grasp_sdk/utils/auth.py +6 -8
- {grasp_sdk-0.1.8.dist-info → grasp_sdk-0.2.0a1.dist-info}/METADATA +2 -3
- grasp_sdk-0.2.0a1.dist-info/RECORD +36 -0
- {grasp_sdk-0.1.8.dist-info → grasp_sdk-0.2.0a1.dist-info}/top_level.txt +1 -0
- grasp_sdk/sandbox/bootstrap-chrome-stable.mjs +0 -69
- grasp_sdk/sandbox/chrome-stable.mjs +0 -424
- grasp_sdk/sandbox/chromium.mjs +0 -395
- grasp_sdk/sandbox/http-proxy.mjs +0 -324
- grasp_sdk-0.1.8.dist-info/RECORD +0 -18
- {grasp_sdk-0.1.8.dist-info → grasp_sdk-0.2.0a1.dist-info}/WHEEL +0 -0
- {grasp_sdk-0.1.8.dist-info → grasp_sdk-0.2.0a1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Test script to verify terminal service updates in Python SDK.
|
|
4
|
+
|
|
5
|
+
This script tests the updated terminal service functionality,
|
|
6
|
+
including the new kill() method and response.json() compatibility.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import sys
|
|
11
|
+
import os
|
|
12
|
+
from unittest.mock import Mock, AsyncMock, patch
|
|
13
|
+
|
|
14
|
+
# Add the parent directory to the path to import grasp_sdk
|
|
15
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
|
16
|
+
|
|
17
|
+
from grasp_sdk.services.terminal import TerminalService, StreamableCommandResult
|
|
18
|
+
from grasp_sdk.services.sandbox import SandboxService, CommandEventEmitter
|
|
19
|
+
from grasp_sdk.services.browser import CDPConnection
|
|
20
|
+
from grasp_sdk.models import ISandboxConfig
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MockCommandEventEmitter(CommandEventEmitter):
|
|
24
|
+
"""Mock command event emitter for testing."""
|
|
25
|
+
|
|
26
|
+
def __init__(self):
|
|
27
|
+
super().__init__()
|
|
28
|
+
self.killed = False
|
|
29
|
+
self.stdout_data = "Test output\n"
|
|
30
|
+
self.stderr_data = "Test error\n"
|
|
31
|
+
|
|
32
|
+
async def wait(self):
|
|
33
|
+
"""Mock wait method."""
|
|
34
|
+
await asyncio.sleep(0.1) # Simulate some processing time
|
|
35
|
+
return {
|
|
36
|
+
'exit_code': 0,
|
|
37
|
+
'stdout': self.stdout_data,
|
|
38
|
+
'stderr': self.stderr_data
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async def kill(self):
|
|
42
|
+
"""Mock kill method."""
|
|
43
|
+
self.killed = True
|
|
44
|
+
print("Command killed successfully")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def test_streamable_command_result():
|
|
48
|
+
"""Test StreamableCommandResult functionality."""
|
|
49
|
+
print("\n=== Testing StreamableCommandResult ===")
|
|
50
|
+
|
|
51
|
+
# Create mock emitter and task
|
|
52
|
+
emitter = MockCommandEventEmitter()
|
|
53
|
+
task = asyncio.create_task(emitter.wait())
|
|
54
|
+
|
|
55
|
+
# Create StreamableCommandResult
|
|
56
|
+
result = StreamableCommandResult(emitter, task)
|
|
57
|
+
|
|
58
|
+
# Test stdout/stderr streams
|
|
59
|
+
print("✓ StreamableCommandResult created successfully")
|
|
60
|
+
|
|
61
|
+
# Test response.json() method
|
|
62
|
+
response_data = await result.json()
|
|
63
|
+
print(f"✓ Response JSON: {response_data}")
|
|
64
|
+
|
|
65
|
+
# Test end() method
|
|
66
|
+
end_result = await result.end()
|
|
67
|
+
print(f"✓ End result: {end_result}")
|
|
68
|
+
|
|
69
|
+
# Test kill() method
|
|
70
|
+
await result.kill()
|
|
71
|
+
print(f"✓ Kill method executed, emitter killed: {emitter.killed}")
|
|
72
|
+
|
|
73
|
+
print("✓ All StreamableCommandResult tests passed!")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def test_terminal_service():
|
|
77
|
+
"""Test TerminalService functionality."""
|
|
78
|
+
print("\n=== Testing TerminalService ===")
|
|
79
|
+
|
|
80
|
+
# Create mock sandbox and connection
|
|
81
|
+
sandbox_config: ISandboxConfig = {
|
|
82
|
+
'key': 'test-key',
|
|
83
|
+
'timeout': 30000,
|
|
84
|
+
'debug': True
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
mock_sandbox = Mock(spec=SandboxService)
|
|
88
|
+
mock_sandbox.is_debug = True
|
|
89
|
+
mock_sandbox.run_command = AsyncMock()
|
|
90
|
+
|
|
91
|
+
mock_connection = Mock(spec=CDPConnection)
|
|
92
|
+
mock_connection.ws_url = 'ws://localhost:8080'
|
|
93
|
+
|
|
94
|
+
# Create terminal service
|
|
95
|
+
terminal = TerminalService(mock_sandbox, mock_connection)
|
|
96
|
+
print("✓ TerminalService created successfully")
|
|
97
|
+
|
|
98
|
+
# Mock websockets.connect
|
|
99
|
+
with patch('grasp_sdk.services.terminal.websockets.connect') as mock_connect:
|
|
100
|
+
mock_ws = AsyncMock()
|
|
101
|
+
mock_ws.open = True
|
|
102
|
+
mock_connect.return_value.__aenter__ = AsyncMock(return_value=mock_ws)
|
|
103
|
+
mock_connect.return_value.__aexit__ = AsyncMock(return_value=None)
|
|
104
|
+
# For direct await usage
|
|
105
|
+
async def mock_connect_func(*args, **kwargs):
|
|
106
|
+
return mock_ws
|
|
107
|
+
mock_connect.side_effect = mock_connect_func
|
|
108
|
+
|
|
109
|
+
# Mock emitter
|
|
110
|
+
mock_emitter = MockCommandEventEmitter()
|
|
111
|
+
mock_sandbox.run_command.return_value = mock_emitter
|
|
112
|
+
|
|
113
|
+
# Test run_command
|
|
114
|
+
result = await terminal.run_command('echo "Hello World"', {'timeout': 5000})
|
|
115
|
+
print("✓ run_command executed successfully")
|
|
116
|
+
|
|
117
|
+
# Verify the command was called with correct options
|
|
118
|
+
mock_sandbox.run_command.assert_called_once()
|
|
119
|
+
call_args = mock_sandbox.run_command.call_args
|
|
120
|
+
command_arg = call_args[0][0]
|
|
121
|
+
options_arg = call_args[0][1]
|
|
122
|
+
|
|
123
|
+
print(f"✓ Command: {command_arg}")
|
|
124
|
+
print(f"✓ Options: {options_arg}")
|
|
125
|
+
|
|
126
|
+
# Verify inBackground option is set correctly
|
|
127
|
+
assert options_arg['inBackground'] == True, "inBackground option should be True"
|
|
128
|
+
assert options_arg['nohup'] == False, "Nohup option should be False"
|
|
129
|
+
print("✓ Command options verified")
|
|
130
|
+
|
|
131
|
+
# Test result methods
|
|
132
|
+
response_data = await result.json()
|
|
133
|
+
print(f"✓ Response JSON: {response_data}")
|
|
134
|
+
|
|
135
|
+
# Test kill method
|
|
136
|
+
await result.kill()
|
|
137
|
+
print("✓ Kill method executed successfully")
|
|
138
|
+
|
|
139
|
+
# Test close method
|
|
140
|
+
terminal.close()
|
|
141
|
+
print("✓ Terminal service closed successfully")
|
|
142
|
+
|
|
143
|
+
print("✓ All TerminalService tests passed!")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
async def test_command_options_compatibility():
|
|
147
|
+
"""Test command options compatibility with TypeScript version."""
|
|
148
|
+
print("\n=== Testing Command Options Compatibility ===")
|
|
149
|
+
|
|
150
|
+
from grasp_sdk.models import ICommandOptions
|
|
151
|
+
|
|
152
|
+
# Test that ICommandOptions supports 'inBackground' parameter
|
|
153
|
+
options: ICommandOptions = {
|
|
154
|
+
'inBackground': True,
|
|
155
|
+
'timeout': 5000,
|
|
156
|
+
'cwd': '/tmp',
|
|
157
|
+
'nohup': False
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
print(f"✓ Command options created: {options}")
|
|
161
|
+
print("✓ 'inBackground' parameter is supported in ICommandOptions")
|
|
162
|
+
|
|
163
|
+
# Test that we can access the inBackground option
|
|
164
|
+
background_value = options.get('inBackground', False)
|
|
165
|
+
assert background_value == True, "inBackground option should be accessible"
|
|
166
|
+
print("✓ inBackground option accessible and correct")
|
|
167
|
+
|
|
168
|
+
print("✓ All command options compatibility tests passed!")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
async def main():
|
|
172
|
+
"""Run all tests."""
|
|
173
|
+
print("Starting Terminal Service Update Tests...")
|
|
174
|
+
|
|
175
|
+
try:
|
|
176
|
+
await test_streamable_command_result()
|
|
177
|
+
await test_terminal_service()
|
|
178
|
+
await test_command_options_compatibility()
|
|
179
|
+
|
|
180
|
+
print("\n🎉 All tests passed! Terminal service updates are working correctly.")
|
|
181
|
+
print("\n📋 Summary of updates:")
|
|
182
|
+
print(" • Added kill() method to StreamableCommandResult")
|
|
183
|
+
print(" • Added response.json() compatibility method")
|
|
184
|
+
print(" • Maintained ICommandOptions to use 'inBackground' for Python compatibility")
|
|
185
|
+
print(" • Improved error handling and WebSocket management")
|
|
186
|
+
print(" • Synchronized timeout values with TypeScript version")
|
|
187
|
+
|
|
188
|
+
except Exception as e:
|
|
189
|
+
print(f"\n❌ Test failed: {e}")
|
|
190
|
+
import traceback
|
|
191
|
+
traceback.print_exc()
|
|
192
|
+
sys.exit(1)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
if __name__ == '__main__':
|
|
196
|
+
asyncio.run(main())
|
grasp_sdk/__init__.py
CHANGED
|
@@ -1,291 +1,183 @@
|
|
|
1
|
-
"""Grasp
|
|
1
|
+
"""Grasp Server Python SDK
|
|
2
2
|
|
|
3
|
-
A Python SDK for
|
|
3
|
+
A Python SDK for Grasp platform providing secure command execution
|
|
4
4
|
and browser automation in isolated cloud environments.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import asyncio
|
|
8
8
|
import signal
|
|
9
|
-
import
|
|
10
|
-
|
|
9
|
+
from typing import Dict, Optional, Any
|
|
10
|
+
import warnings
|
|
11
11
|
|
|
12
12
|
# Import utilities
|
|
13
|
-
from .utils.logger import
|
|
14
|
-
|
|
15
|
-
from
|
|
16
|
-
from .
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
SandboxStatus,
|
|
13
|
+
from .utils.logger import get_logger
|
|
14
|
+
|
|
15
|
+
# Import all classes from grasp module
|
|
16
|
+
from .grasp import (
|
|
17
|
+
GraspServer,
|
|
18
|
+
GraspBrowser,
|
|
19
|
+
GraspTerminal,
|
|
20
|
+
GraspSession,
|
|
21
|
+
Grasp,
|
|
22
|
+
_servers,
|
|
23
|
+
shutdown,
|
|
25
24
|
)
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
# Import CDPConnection for type annotation
|
|
27
|
+
from .services.browser import CDPConnection
|
|
28
|
+
|
|
29
|
+
__version__ = "0.2.0a1"
|
|
28
30
|
__author__ = "Grasp Team"
|
|
29
31
|
__email__ = "team@grasp.dev"
|
|
30
32
|
|
|
33
|
+
# Global server registry (re-exported from grasp module)
|
|
34
|
+
_servers: Dict[str, GraspServer] = _servers
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
"""Main Grasp E2B class for browser automation."""
|
|
34
|
-
|
|
35
|
-
def __init__(self, sandbox_config: Optional[Dict[str, Any]] = None):
|
|
36
|
-
"""Initialize GraspServer with configuration.
|
|
37
|
-
|
|
38
|
-
Args:
|
|
39
|
-
sandbox_config: Optional sandbox configuration overrides
|
|
40
|
-
"""
|
|
41
|
-
if sandbox_config is None:
|
|
42
|
-
sandbox_config = {}
|
|
43
|
-
|
|
44
|
-
# Extract browser-specific options
|
|
45
|
-
browser_type = sandbox_config.pop('type', 'chromium')
|
|
46
|
-
headless = sandbox_config.pop('headless', True)
|
|
47
|
-
adblock = sandbox_config.pop('adblock', False)
|
|
48
|
-
logLevel = sandbox_config.pop('logLevel', '')
|
|
49
|
-
keepAliveMS = sandbox_config.pop('keepAliveMS', 0)
|
|
50
|
-
|
|
51
|
-
# Set default log level
|
|
52
|
-
if not logLevel:
|
|
53
|
-
logLevel = 'debug' if sandbox_config.get('debug', False) else 'info'
|
|
54
|
-
|
|
55
|
-
self.__browser_type = browser_type
|
|
56
|
-
|
|
57
|
-
# Create browser task
|
|
58
|
-
self.__browser_config = {
|
|
59
|
-
'headless': headless,
|
|
60
|
-
'envs': {
|
|
61
|
-
'ADBLOCK': 'true' if adblock else 'false',
|
|
62
|
-
'KEEP_ALIVE_MS': str(keepAliveMS),
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
config = get_config()
|
|
67
|
-
config['sandbox'].update(sandbox_config)
|
|
68
|
-
self.config = config
|
|
69
|
-
|
|
70
|
-
logger = config['logger']
|
|
71
|
-
logger['level'] = logLevel
|
|
72
|
-
|
|
73
|
-
# Initialize logger first
|
|
74
|
-
init_logger(logger)
|
|
75
|
-
self.logger = get_logger().child('GraspE2B')
|
|
76
|
-
|
|
77
|
-
self.browser_service: Optional[BrowserService] = None
|
|
78
|
-
|
|
79
|
-
self.logger.info('GraspE2B initialized')
|
|
80
|
-
|
|
81
|
-
async def __aenter__(self):
|
|
82
|
-
connection = await self.create_browser_task()
|
|
83
|
-
|
|
84
|
-
# Register server
|
|
85
|
-
# if connection['id']:
|
|
86
|
-
# _servers[connection['id']] = self
|
|
87
|
-
|
|
88
|
-
return connection
|
|
89
|
-
|
|
90
|
-
async def __aexit__(self, exc_type, exc, tb):
|
|
91
|
-
if self.browser_service and self.browser_service.id:
|
|
92
|
-
service_id = self.browser_service.id
|
|
93
|
-
self.logger.info(f'Closing browser service {service_id}')
|
|
94
|
-
await _servers[service_id].cleanup()
|
|
95
|
-
del _servers[service_id]
|
|
96
|
-
|
|
97
|
-
@property
|
|
98
|
-
def sandbox(self) -> Optional[SandboxService]:
|
|
99
|
-
"""Get the underlying sandbox service.
|
|
100
|
-
|
|
101
|
-
Returns:
|
|
102
|
-
SandboxService instance or None
|
|
103
|
-
"""
|
|
104
|
-
return self.browser_service.get_sandbox() if self.browser_service else None
|
|
105
|
-
|
|
106
|
-
def get_status(self) -> Optional[SandboxStatus]:
|
|
107
|
-
"""Get current sandbox status.
|
|
108
|
-
|
|
109
|
-
Returns:
|
|
110
|
-
Sandbox status or None
|
|
111
|
-
"""
|
|
112
|
-
return self.sandbox.get_status() if self.sandbox else None
|
|
113
|
-
|
|
114
|
-
def get_sandbox_id(self) -> Optional[str]:
|
|
115
|
-
"""Get sandbox ID.
|
|
116
|
-
|
|
117
|
-
Returns:
|
|
118
|
-
Sandbox ID or None
|
|
119
|
-
"""
|
|
120
|
-
return self.sandbox.get_sandbox_id() if self.sandbox else None
|
|
121
|
-
|
|
122
|
-
async def create_browser_task(
|
|
123
|
-
self,
|
|
124
|
-
) -> Dict[str, Any]:
|
|
125
|
-
"""Create and launch a browser task.
|
|
126
|
-
|
|
127
|
-
Args:
|
|
128
|
-
browser_type: Type of browser to launch
|
|
129
|
-
config: Browser configuration overrides
|
|
130
|
-
|
|
131
|
-
Returns:
|
|
132
|
-
Dictionary containing browser connection info
|
|
133
|
-
|
|
134
|
-
Raises:
|
|
135
|
-
RuntimeError: If browser service is already initialized
|
|
136
|
-
"""
|
|
137
|
-
if self.browser_service:
|
|
138
|
-
raise RuntimeError('Browser service can only be initialized once')
|
|
139
|
-
|
|
140
|
-
config = self.__browser_config
|
|
141
|
-
browser_type = self.__browser_type
|
|
142
|
-
|
|
143
|
-
if config is None:
|
|
144
|
-
config = {}
|
|
145
|
-
|
|
146
|
-
# Create base browser config
|
|
147
|
-
browser_config: IBrowserConfig = {
|
|
148
|
-
'headless': True,
|
|
149
|
-
'launchTimeout': 30000,
|
|
150
|
-
'args': [
|
|
151
|
-
'--disable-web-security',
|
|
152
|
-
'--disable-features=VizDisplayCompositor',
|
|
153
|
-
],
|
|
154
|
-
'envs': {},
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
# Apply user config overrides with type safety
|
|
158
|
-
if 'headless' in config:
|
|
159
|
-
browser_config['headless'] = config['headless']
|
|
160
|
-
if 'launchTimeout' in config:
|
|
161
|
-
browser_config['launchTimeout'] = config['launchTimeout']
|
|
162
|
-
if 'args' in config:
|
|
163
|
-
browser_config['args'] = config['args']
|
|
164
|
-
if 'envs' in config:
|
|
165
|
-
browser_config['envs'] = config['envs']
|
|
166
|
-
|
|
167
|
-
self.browser_service = BrowserService(
|
|
168
|
-
self.config['sandbox'],
|
|
169
|
-
browser_config
|
|
170
|
-
)
|
|
171
|
-
await self.browser_service.initialize(browser_type)
|
|
172
|
-
|
|
173
|
-
# Register server
|
|
174
|
-
_servers[str(self.browser_service.id)] = self
|
|
175
|
-
self.logger.info("🚀 Browser service initialized", {
|
|
176
|
-
'id': self.browser_service.id,
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
self.logger.info('🌐 Launching Chromium browser with CDP...')
|
|
180
|
-
cdp_connection = await self.browser_service.launch_browser()
|
|
181
|
-
|
|
182
|
-
self.logger.info('✅ Browser launched successfully!')
|
|
183
|
-
self.logger.debug(
|
|
184
|
-
f'CDP Connection Info (wsUrl: {cdp_connection.ws_url}, httpUrl: {cdp_connection.http_url})'
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
return {
|
|
188
|
-
'id': self.browser_service.id,
|
|
189
|
-
'ws_url': cdp_connection.ws_url,
|
|
190
|
-
'http_url': cdp_connection.http_url
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
async def cleanup(self) -> None:
|
|
194
|
-
"""Cleanup resources.
|
|
195
|
-
|
|
196
|
-
Returns:
|
|
197
|
-
Promise that resolves when cleanup is complete
|
|
198
|
-
"""
|
|
199
|
-
self.logger.info('Starting cleanup process')
|
|
200
|
-
|
|
201
|
-
try:
|
|
202
|
-
# Cleanup browser service
|
|
203
|
-
if self.browser_service:
|
|
204
|
-
await self.browser_service.cleanup()
|
|
205
|
-
|
|
206
|
-
self.logger.info('Cleanup completed successfully')
|
|
207
|
-
except Exception as error:
|
|
208
|
-
self.logger.error(f'Cleanup failed: {error}')
|
|
209
|
-
raise
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
# Global server registry
|
|
213
|
-
_servers: Dict[str, GraspServer] = {}
|
|
214
|
-
|
|
215
|
-
|
|
36
|
+
# Deprecated functions for backward compatibility
|
|
216
37
|
async def launch_browser(
|
|
217
38
|
options: Optional[Dict[str, Any]] = None
|
|
218
|
-
) ->
|
|
39
|
+
) -> CDPConnection:
|
|
219
40
|
"""Launch a browser instance.
|
|
220
41
|
|
|
42
|
+
.. deprecated::
|
|
43
|
+
Use grasp.launch() instead. This method will be removed in a future version.
|
|
44
|
+
|
|
221
45
|
Args:
|
|
222
46
|
options: Launch options including type, headless, adblock settings
|
|
223
47
|
|
|
224
48
|
Returns:
|
|
225
49
|
Dictionary containing connection information
|
|
226
|
-
"""
|
|
50
|
+
"""
|
|
51
|
+
import warnings
|
|
52
|
+
warnings.warn(
|
|
53
|
+
"launch_browser() is deprecated. Use grasp.launch() instead.",
|
|
54
|
+
DeprecationWarning,
|
|
55
|
+
stacklevel=2
|
|
56
|
+
)
|
|
227
57
|
# Create server instance
|
|
228
58
|
server = GraspServer(options)
|
|
229
|
-
|
|
59
|
+
|
|
230
60
|
connection = await server.create_browser_task()
|
|
231
|
-
|
|
232
|
-
# Register server
|
|
233
|
-
if connection['id']:
|
|
234
|
-
_servers[connection['id']] = server
|
|
235
|
-
|
|
61
|
+
|
|
236
62
|
return connection
|
|
237
63
|
|
|
238
64
|
|
|
239
|
-
async def _graceful_shutdown(
|
|
240
|
-
"""
|
|
65
|
+
async def _graceful_shutdown(reason: str = 'SIGTERM') -> None:
|
|
66
|
+
"""Gracefully shutdown all servers.
|
|
241
67
|
|
|
242
68
|
Args:
|
|
243
|
-
|
|
69
|
+
reason: Reason for shutdown
|
|
244
70
|
"""
|
|
245
|
-
|
|
71
|
+
import os
|
|
72
|
+
logger = get_logger().child('shutdown')
|
|
73
|
+
logger.info(f'Graceful shutdown initiated: {reason}')
|
|
74
|
+
|
|
75
|
+
if not _servers:
|
|
76
|
+
logger.info('No active servers to shutdown')
|
|
77
|
+
# 强制终止进程
|
|
78
|
+
os._exit(0)
|
|
79
|
+
return
|
|
246
80
|
|
|
247
|
-
|
|
248
|
-
for server_id in list(_servers.keys()):
|
|
249
|
-
await _servers[server_id].cleanup()
|
|
250
|
-
del _servers[server_id]
|
|
81
|
+
logger.info(f'Shutting down {len(_servers)} active servers...')
|
|
251
82
|
|
|
252
|
-
|
|
253
|
-
|
|
83
|
+
# Create cleanup tasks for all servers
|
|
84
|
+
cleanup_tasks = []
|
|
85
|
+
for server_id, server in _servers.items():
|
|
86
|
+
logger.info(f'Scheduling cleanup for server {server_id}')
|
|
87
|
+
cleanup_tasks.append(server.cleanup())
|
|
88
|
+
|
|
89
|
+
# Wait for all cleanups to complete
|
|
90
|
+
try:
|
|
91
|
+
await asyncio.gather(*cleanup_tasks, return_exceptions=True)
|
|
92
|
+
logger.info('All servers cleaned up successfully')
|
|
93
|
+
except Exception as error:
|
|
94
|
+
logger.error(f'Error during cleanup: {error}')
|
|
95
|
+
|
|
96
|
+
# Clear the servers registry
|
|
97
|
+
_servers.clear()
|
|
98
|
+
logger.info('Graceful shutdown completed')
|
|
99
|
+
|
|
100
|
+
# 强制终止进程,确保不会被其他异步任务阻塞
|
|
101
|
+
os._exit(0)
|
|
254
102
|
|
|
255
103
|
|
|
256
104
|
def _setup_signal_handlers() -> None:
|
|
257
105
|
"""Setup signal handlers for graceful shutdown."""
|
|
258
|
-
def signal_handler(
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
106
|
+
def signal_handler(signame):
|
|
107
|
+
logger = get_logger().child('signal')
|
|
108
|
+
logger.info(f'Received signal {signame}, initiating graceful shutdown...')
|
|
109
|
+
|
|
110
|
+
# 创建关闭任务
|
|
111
|
+
asyncio.create_task(_graceful_shutdown(signame))
|
|
112
|
+
|
|
113
|
+
# 尝试使用 asyncio 的信号处理器(更适合异步环境)
|
|
114
|
+
try:
|
|
115
|
+
loop = asyncio.get_running_loop()
|
|
116
|
+
for sig in [signal.SIGTERM, signal.SIGINT]:
|
|
117
|
+
loop.add_signal_handler(sig, lambda s=sig: signal_handler(signal.Signals(s).name))
|
|
118
|
+
except RuntimeError:
|
|
119
|
+
# 如果没有运行的事件循环,使用传统方式
|
|
120
|
+
def fallback_handler(signum, frame):
|
|
121
|
+
signal_name = signal.Signals(signum).name
|
|
122
|
+
logger = get_logger().child('signal')
|
|
123
|
+
logger.info(f'Received {signal_name}, but no event loop is running')
|
|
124
|
+
import os
|
|
125
|
+
os._exit(1)
|
|
126
|
+
|
|
127
|
+
signal.signal(signal.SIGTERM, fallback_handler)
|
|
128
|
+
signal.signal(signal.SIGINT, fallback_handler)
|
|
265
129
|
|
|
266
130
|
|
|
267
|
-
|
|
131
|
+
async def shutdown(connection: Optional[str] = None) -> None:
|
|
132
|
+
"""Shutdown Grasp servers (deprecated).
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
connection: Optional connection ID to shutdown specific server
|
|
136
|
+
|
|
137
|
+
Deprecated:
|
|
138
|
+
This function is deprecated. Use server.cleanup() or context managers instead.
|
|
139
|
+
"""
|
|
140
|
+
warnings.warn(
|
|
141
|
+
"shutdown() is deprecated. Use server.cleanup() or context managers instead.",
|
|
142
|
+
DeprecationWarning,
|
|
143
|
+
stacklevel=2
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
logger = get_logger().child('shutdown')
|
|
147
|
+
|
|
148
|
+
if connection:
|
|
149
|
+
# Shutdown specific connection
|
|
150
|
+
if connection in _servers:
|
|
151
|
+
logger.info(f'Shutting down server {connection}')
|
|
152
|
+
await _servers[connection].cleanup()
|
|
153
|
+
else:
|
|
154
|
+
logger.warn(f'Server {connection} not found')
|
|
155
|
+
else:
|
|
156
|
+
# Shutdown all servers
|
|
157
|
+
await _graceful_shutdown('USER_SHUTDOWN')
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# Setup signal handlers when module is imported
|
|
268
161
|
_setup_signal_handlers()
|
|
269
162
|
|
|
270
|
-
|
|
271
163
|
# Export all public APIs
|
|
272
164
|
__all__ = [
|
|
273
165
|
'GraspServer',
|
|
166
|
+
'Grasp',
|
|
167
|
+
'GraspSession',
|
|
168
|
+
'GraspBrowser',
|
|
169
|
+
'GraspTerminal',
|
|
274
170
|
'launch_browser',
|
|
275
|
-
'
|
|
276
|
-
'IBrowserConfig',
|
|
277
|
-
'ICommandOptions',
|
|
278
|
-
'IScriptOptions',
|
|
279
|
-
'SandboxStatus',
|
|
280
|
-
'get_config',
|
|
281
|
-
'init_logger',
|
|
282
|
-
'get_logger',
|
|
283
|
-
'BrowserService',
|
|
284
|
-
'SandboxService',
|
|
171
|
+
'shutdown',
|
|
285
172
|
]
|
|
286
173
|
|
|
287
|
-
|
|
288
174
|
# Default export equivalent
|
|
289
175
|
default = {
|
|
290
176
|
'GraspServer': GraspServer,
|
|
177
|
+
'Grasp': Grasp,
|
|
178
|
+
'GraspSession': GraspSession,
|
|
179
|
+
'GraspBrowser': GraspBrowser,
|
|
180
|
+
'GraspTerminal': GraspTerminal,
|
|
181
|
+
'launch_browser': launch_browser,
|
|
182
|
+
'shutdown': shutdown,
|
|
291
183
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Grasp SDK classes and utilities."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict
|
|
4
|
+
|
|
5
|
+
from .server import GraspServer
|
|
6
|
+
from .browser import GraspBrowser
|
|
7
|
+
from .terminal import GraspTerminal
|
|
8
|
+
from .session import GraspSession
|
|
9
|
+
from .index import Grasp
|
|
10
|
+
from .utils import (
|
|
11
|
+
_servers,
|
|
12
|
+
shutdown,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
# Export the global servers registry
|
|
16
|
+
_servers: Dict[str, GraspServer] = _servers
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
'GraspServer',
|
|
20
|
+
'GraspBrowser',
|
|
21
|
+
'GraspTerminal',
|
|
22
|
+
'GraspSession',
|
|
23
|
+
'Grasp',
|
|
24
|
+
'shutdown',
|
|
25
|
+
'_servers',
|
|
26
|
+
]
|