grasp-sdk 0.2.0a1__tar.gz → 0.2.0b2__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.

Potentially problematic release.


This version of grasp-sdk might be problematic. Click here for more details.

Files changed (49) hide show
  1. {grasp_sdk-0.2.0a1/grasp_sdk.egg-info → grasp_sdk-0.2.0b2}/PKG-INFO +1 -1
  2. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/examples/example_async_context.py +1 -1
  3. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/examples/example_grasp_usage.py +1 -1
  4. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/examples/grasp_terminal.py +3 -2
  5. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/examples/test_grasp_classes.py +1 -2
  6. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/examples/test_removed_methods.py +1 -1
  7. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/examples/test_terminal_updates.py +2 -2
  8. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk/__init__.py +1 -3
  9. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk/grasp/__init__.py +0 -2
  10. grasp_sdk-0.2.0b2/grasp_sdk/grasp/browser.py +162 -0
  11. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk/grasp/index.py +11 -6
  12. grasp_sdk-0.2.0b2/grasp_sdk/grasp/session.py +146 -0
  13. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk/models/__init__.py +10 -5
  14. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk/services/browser.py +5 -3
  15. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk/services/filesystem.py +30 -1
  16. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk/services/sandbox.py +14 -7
  17. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk/services/terminal.py +5 -0
  18. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk/utils/__init__.py +1 -2
  19. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk/utils/config.py +2 -32
  20. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2/grasp_sdk.egg-info}/PKG-INFO +1 -1
  21. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk.egg-info/SOURCES.txt +0 -6
  22. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/pyproject.toml +1 -1
  23. grasp_sdk-0.2.0a1/examples/test_get_replay_screenshots.py +0 -90
  24. grasp_sdk-0.2.0a1/examples/test_shutdown_deprecation.py +0 -62
  25. grasp_sdk-0.2.0a1/grasp_sdk/grasp/browser.py +0 -69
  26. grasp_sdk-0.2.0a1/grasp_sdk/grasp/session.py +0 -108
  27. grasp_sdk-0.2.0a1/grasp_sdk/grasp/terminal.py +0 -32
  28. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/MANIFEST.in +0 -0
  29. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/README.md +0 -0
  30. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/build_and_publish.py +0 -0
  31. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/examples/example_binary_file_support.py +0 -0
  32. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/examples/example_readfile_usage.py +0 -0
  33. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/examples/grasp_usage.py +0 -0
  34. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/examples/test_async_context.py +0 -0
  35. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/examples/test_python_script.py +0 -0
  36. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk/grasp/server.py +0 -0
  37. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk/grasp/utils.py +0 -0
  38. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk/services/__init__.py +0 -0
  39. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk/utils/auth.py +0 -0
  40. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk/utils/logger.py +0 -0
  41. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk.egg-info/dependency_links.txt +0 -0
  42. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk.egg-info/entry_points.txt +0 -0
  43. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk.egg-info/not-zip-safe +0 -0
  44. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk.egg-info/requires.txt +0 -0
  45. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/grasp_sdk.egg-info/top_level.txt +0 -0
  46. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/py.typed +0 -0
  47. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/requirements.txt +0 -0
  48. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/setup.cfg +0 -0
  49. {grasp_sdk-0.2.0a1 → grasp_sdk-0.2.0b2}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: grasp_sdk
3
- Version: 0.2.0a1
3
+ Version: 0.2.0b2
4
4
  Summary: Python SDK for Grasp E2B - Browser automation and sandbox management
5
5
  Home-page: https://github.com/grasp-team/grasp-e2b
6
6
  Author: Grasp Team
@@ -31,7 +31,7 @@ async def example_with_context_manager():
31
31
 
32
32
  # 创建终端
33
33
  print("💻 创建终端...")
34
- terminal = session.terminal.create()
34
+ terminal = session.terminal
35
35
  print(f"✅ 终端已创建: {type(terminal).__name__}")
36
36
 
37
37
  # 访问文件系统
@@ -34,7 +34,7 @@ async def example_grasp_usage():
34
34
  print(f"✓ Browser WebSocket endpoint: {browser_endpoint}")
35
35
 
36
36
  # Create terminal
37
- terminal = session.terminal.create()
37
+ terminal = session.terminal
38
38
  print("✓ Terminal service created")
39
39
 
40
40
  # Access file system
@@ -36,7 +36,8 @@ async def main():
36
36
  # 'debug': True,
37
37
  },
38
38
  'keepAliveMS': 10000,
39
- 'timeout': 3600000, # 容器最长运行1小时(最大值可以为一天 86400000)
39
+ # 'logLevel': 'error',
40
+ #'timeout': 3600000, # 容器最长运行1小时(最大值可以为一天 86400000)
40
41
  })
41
42
 
42
43
  connection = session.browser.connection
@@ -44,7 +45,7 @@ async def main():
44
45
 
45
46
  # Create terminal connection
46
47
  print("📡 Creating terminal connection...")
47
- terminal = session.terminal.create()
48
+ terminal = session.terminal
48
49
 
49
50
  print("✅ Terminal connected!")
50
51
 
@@ -2,7 +2,7 @@
2
2
  """测试 Grasp 类的功能"""
3
3
 
4
4
  import os
5
- from grasp_sdk import Grasp, GraspSession, GraspBrowser, GraspTerminal
5
+ from grasp_sdk import Grasp, GraspSession, GraspBrowser
6
6
 
7
7
  def test_grasp_classes():
8
8
  """测试所有 Grasp 类的基本功能"""
@@ -25,7 +25,6 @@ def test_grasp_classes():
25
25
  print(f" ✓ Grasp 类可用: {Grasp is not None}")
26
26
  print(f" ✓ GraspSession 类可用: {GraspSession is not None}")
27
27
  print(f" ✓ GraspBrowser 类可用: {GraspBrowser is not None}")
28
- print(f" ✓ GraspTerminal 类可用: {GraspTerminal is not None}")
29
28
 
30
29
  # 测试方法可用性
31
30
  print("\n3. 测试方法可用性:")
@@ -17,7 +17,7 @@ def test_removed_methods():
17
17
  print("=" * 30)
18
18
 
19
19
  try:
20
- from grasp_sdk import Grasp, GraspSession, GraspBrowser, GraspTerminal
20
+ from grasp_sdk import Grasp, GraspSession, GraspBrowser
21
21
  print("✅ 核心类导入成功")
22
22
  except ImportError as e:
23
23
  print(f"❌ 核心类导入失败: {e}")
@@ -111,7 +111,7 @@ async def test_terminal_service():
111
111
  mock_sandbox.run_command.return_value = mock_emitter
112
112
 
113
113
  # Test run_command
114
- result = await terminal.run_command('echo "Hello World"', {'timeout': 5000})
114
+ result = await terminal.run_command('echo "Hello World"', {'timeout_ms': 5000})
115
115
  print("✓ run_command executed successfully")
116
116
 
117
117
  # Verify the command was called with correct options
@@ -152,7 +152,7 @@ async def test_command_options_compatibility():
152
152
  # Test that ICommandOptions supports 'inBackground' parameter
153
153
  options: ICommandOptions = {
154
154
  'inBackground': True,
155
- 'timeout': 5000,
155
+ 'timeout_ms': 5000,
156
156
  'cwd': '/tmp',
157
157
  'nohup': False
158
158
  }
@@ -16,7 +16,6 @@ from .utils.logger import get_logger
16
16
  from .grasp import (
17
17
  GraspServer,
18
18
  GraspBrowser,
19
- GraspTerminal,
20
19
  GraspSession,
21
20
  Grasp,
22
21
  _servers,
@@ -26,7 +25,7 @@ from .grasp import (
26
25
  # Import CDPConnection for type annotation
27
26
  from .services.browser import CDPConnection
28
27
 
29
- __version__ = "0.2.0a1"
28
+ __version__ = "0.2.0b2"
30
29
  __author__ = "Grasp Team"
31
30
  __email__ = "team@grasp.dev"
32
31
 
@@ -177,7 +176,6 @@ default = {
177
176
  'Grasp': Grasp,
178
177
  'GraspSession': GraspSession,
179
178
  'GraspBrowser': GraspBrowser,
180
- 'GraspTerminal': GraspTerminal,
181
179
  'launch_browser': launch_browser,
182
180
  'shutdown': shutdown,
183
181
  }
@@ -4,7 +4,6 @@ from typing import Dict
4
4
 
5
5
  from .server import GraspServer
6
6
  from .browser import GraspBrowser
7
- from .terminal import GraspTerminal
8
7
  from .session import GraspSession
9
8
  from .index import Grasp
10
9
  from .utils import (
@@ -18,7 +17,6 @@ _servers: Dict[str, GraspServer] = _servers
18
17
  __all__ = [
19
18
  'GraspServer',
20
19
  'GraspBrowser',
21
- 'GraspTerminal',
22
20
  'GraspSession',
23
21
  'Grasp',
24
22
  'shutdown',
@@ -0,0 +1,162 @@
1
+ """GraspBrowser class for browser interface."""
2
+
3
+ import aiohttp
4
+ import os
5
+ from typing import Optional, Dict, Any, TYPE_CHECKING
6
+
7
+ from ..services.browser import CDPConnection
8
+
9
+ if TYPE_CHECKING:
10
+ from .session import GraspSession
11
+
12
+
13
+ class GraspBrowser:
14
+ """Browser interface for Grasp session."""
15
+
16
+ def __init__(self, connection: CDPConnection, session: Optional['GraspSession'] = None):
17
+ """Initialize GraspBrowser.
18
+
19
+ Args:
20
+ connection: CDP connection instance
21
+ session: GraspSession instance for accessing terminal and files services
22
+ """
23
+ self.connection = connection
24
+ self.session = session
25
+
26
+ def get_host(self) -> str:
27
+ """Get browser host.
28
+
29
+ Returns:
30
+ Host for browser connection
31
+ """
32
+ from urllib.parse import urlparse
33
+ return urlparse(self.connection.http_url).netloc
34
+
35
+ def get_endpoint(self) -> str:
36
+ """Get browser WebSocket endpoint URL.
37
+
38
+ Returns:
39
+ WebSocket URL for browser connection
40
+ """
41
+ return self.connection.ws_url
42
+
43
+ async def get_current_page_target_info(self) -> Optional[Dict[str, Any]]:
44
+ """Get current page target information.
45
+
46
+ Warning:
47
+ This method is experimental and may change or be removed in future versions.
48
+ Use with caution in production environments.
49
+
50
+ Returns:
51
+ Dictionary containing target info and last screenshot, or None if failed
52
+ """
53
+ host = self.connection.http_url
54
+ api = f"{host}/api/page/info"
55
+
56
+ try:
57
+ async with aiohttp.ClientSession() as session:
58
+ async with session.get(api) as response:
59
+ if not response.ok:
60
+ return None
61
+
62
+ data = await response.json()
63
+ page_info = data.get('pageInfo', {})
64
+ last_screenshot = page_info.get('lastScreenshot')
65
+ session_id = page_info.get('sessionId')
66
+ targets = page_info.get('targets', {})
67
+ target_id = page_info.get('targetId')
68
+ page_loaded = page_info.get('pageLoaded')
69
+
70
+ current_target = {}
71
+ for target in targets.values():
72
+ if target.get('targetId') == target_id:
73
+ current_target = target
74
+ break
75
+
76
+ return {
77
+ 'targetId': target_id,
78
+ 'sessionId': session_id,
79
+ 'pageLoaded': page_loaded,
80
+ **current_target,
81
+ 'lastScreenshot': last_screenshot,
82
+ 'targets': targets,
83
+ }
84
+ except Exception:
85
+ return None
86
+
87
+ async def download_replay_video(self, local_path: str) -> None:
88
+ """Download replay video from screenshots.
89
+
90
+ Args:
91
+ local_path: Local path to save the video file
92
+
93
+ Raises:
94
+ RuntimeError: If session is not available or video generation fails
95
+ """
96
+ if not self.session:
97
+ raise RuntimeError("Session is required for download_replay_video")
98
+
99
+ # Create terminal instance
100
+ terminal = self.session.terminal
101
+
102
+ # Generate file list for ffmpeg
103
+ command1 = await terminal.run_command(
104
+ "cd /home/user/downloads/grasp-screenshots && ls -1 | grep -v '^filelist.txt$' | sort | awk '{print \"file '\''\" $0 \"'\''\"}'> filelist.txt"
105
+ )
106
+ await command1.end()
107
+
108
+ # Generate video using ffmpeg
109
+ command2 = await terminal.run_command(
110
+ "cd /home/user/downloads/grasp-screenshots && ffmpeg -r 25 -f concat -safe 0 -i filelist.txt -vsync vfr -pix_fmt yuv420p output.mp4"
111
+ )
112
+ await command2.end()
113
+
114
+ # Determine final local path
115
+ if not local_path.endswith('.mp4'):
116
+ local_path = os.path.join(local_path, 'output.mp4')
117
+
118
+ # Download the generated video file
119
+ await self.session.files.download_file(
120
+ '/home/user/downloads/grasp-screenshots/output.mp4',
121
+ local_path
122
+ )
123
+
124
+ async def get_liveview_streaming_url(self) -> Optional[str]:
125
+ """Get liveview streaming URL.
126
+
127
+ Returns:
128
+ Liveview streaming URL or None if not ready
129
+ """
130
+ host = self.connection.http_url
131
+ api = f"{host}/api/session/liveview/stream"
132
+
133
+ try:
134
+ async with aiohttp.ClientSession() as session:
135
+ async with session.get(api) as response:
136
+ if not response.ok:
137
+ return None
138
+ return api
139
+ except Exception:
140
+ return None
141
+
142
+ async def get_liveview_page_url(self) -> Optional[str]:
143
+ """Get liveview page URL.
144
+
145
+ Warning:
146
+ This method is experimental and may change or be removed in future versions.
147
+ Use with caution in production environments.
148
+
149
+ Returns:
150
+ Liveview page URL or None if not ready
151
+ """
152
+ host = self.connection.http_url
153
+ api = f"{host}/api/session/liveview/preview"
154
+
155
+ try:
156
+ async with aiohttp.ClientSession() as session:
157
+ async with session.get(api) as response:
158
+ if not response.ok:
159
+ return None
160
+ return api
161
+ except Exception:
162
+ return None
@@ -5,6 +5,7 @@ from typing import Dict, Optional, Any
5
5
 
6
6
  from .server import GraspServer
7
7
  from .session import GraspSession
8
+ from ..utils.config import get_config
8
9
 
9
10
 
10
11
  class Grasp:
@@ -32,12 +33,13 @@ class Grasp:
32
33
  Returns:
33
34
  GraspSession instance
34
35
  """
36
+ config = get_config()
35
37
  browser_options = options.get('browser', {})
36
- browser_options['key'] = self.key
37
- browser_options['keepAliveMS'] = options.get('keepAliveMS', 10000)
38
- browser_options['timeout'] = options.get('timeout', 900000)
39
- browser_options['debug'] = options.get('debug', False)
40
- browser_options['logLevel'] = options.get('logLevel', 'info')
38
+ browser_options['key'] = self.key or config['sandbox']['key']
39
+ browser_options['keepAliveMS'] = options.get('keepAliveMS', config['sandbox']['keepAliveMs'])
40
+ browser_options['timeout'] = options.get('timeout', config['sandbox']['timeout'])
41
+ browser_options['debug'] = options.get('debug', config['sandbox']['debug'])
42
+ browser_options['logLevel'] = options.get('logLevel', config['logger']['level'])
41
43
 
42
44
  server = GraspServer(browser_options)
43
45
  cdp_connection = await server.create_browser_task()
@@ -116,7 +118,10 @@ class Grasp:
116
118
 
117
119
  if not cdp_connection:
118
120
  # Create new server with no timeout for existing sessions
119
- server = GraspServer({'timeout': 0})
121
+ server = GraspServer({
122
+ 'timeout': 0,
123
+ 'key': self.key,
124
+ })
120
125
  cdp_connection = await server.connect_browser_task(session_id)
121
126
 
122
127
  return GraspSession(cdp_connection)
@@ -0,0 +1,146 @@
1
+ """GraspSession class for session management."""
2
+
3
+ import asyncio
4
+ import websockets
5
+ from typing import Optional
6
+ from .browser import GraspBrowser
7
+ from ..services.terminal import TerminalService
8
+ from ..services.browser import CDPConnection
9
+ from ..services.filesystem import FileSystemService
10
+ from ..utils.logger import get_logger
11
+
12
+
13
+ class GraspSession:
14
+ """Main session interface providing access to browser, terminal, and files."""
15
+
16
+ def __init__(self, connection: CDPConnection):
17
+ """Initialize GraspSession.
18
+
19
+ Args:
20
+ connection: CDP connection instance
21
+ """
22
+ self.connection = connection
23
+ self.browser = GraspBrowser(connection, self)
24
+
25
+ # Create files service
26
+ connection_id = connection.id
27
+ try:
28
+ from .utils import _servers
29
+ server = _servers[connection_id]
30
+ if server.sandbox is None:
31
+ raise RuntimeError('Sandbox is not available for file system service')
32
+ self.files = FileSystemService(server.sandbox, connection)
33
+ self.terminal = TerminalService(server.sandbox, connection)
34
+ except (ImportError, KeyError) as e:
35
+ # In test environment or when server is not available, create a mock files service
36
+ self.logger.debug(f"Warning: Files service not available: {e}")
37
+ raise e
38
+
39
+ # WebSocket connection for keep-alive
40
+ self._ws = None
41
+ self._is_closed = False
42
+
43
+ self.logger = get_logger().child('GraspSession')
44
+
45
+ # Initialize WebSocket connection and start keep-alive
46
+ # self.logger.debug('create session...')
47
+ asyncio.create_task(self._initialize_websocket())
48
+
49
+ async def _initialize_websocket(self) -> None:
50
+ """Initialize WebSocket connection and start keep-alive."""
51
+ try:
52
+ self._ws = await websockets.connect(self.connection.ws_url)
53
+ if not self._is_closed:
54
+ await self._keep_alive()
55
+ else:
56
+ await self._ws.close()
57
+ except Exception as e:
58
+ self.logger.debug(f"Failed to initialize WebSocket connection: {e}")
59
+
60
+ async def _keep_alive(self) -> None:
61
+ """Keep WebSocket connection alive with periodic ping."""
62
+ ws = self._ws
63
+ await asyncio.sleep(10) # 10 seconds interval, same as TypeScript version
64
+ if ws and ws.close_code is None:
65
+ try:
66
+ # Send ping and wait for pong response
67
+ pong_waiter = ws.ping()
68
+ await pong_waiter
69
+ # Recursively call keep_alive after receiving pong
70
+ await self._keep_alive()
71
+ except Exception as e:
72
+ self.logger.debug(f"Keep-alive ping failed: {e}")
73
+
74
+ @property
75
+ def id(self) -> str:
76
+ """Get session ID.
77
+
78
+ Returns:
79
+ Session ID
80
+ """
81
+ return self.connection.id
82
+
83
+ def is_running(self) -> bool:
84
+ """Check if the session is currently running.
85
+
86
+ Returns:
87
+ True if the session is running, False otherwise
88
+ """
89
+ try:
90
+ from .utils import _servers
91
+ from ..models import SandboxStatus
92
+ server = _servers[self.id]
93
+ if server.sandbox is None:
94
+ return False
95
+ status = server.sandbox.get_status()
96
+ return status == SandboxStatus.RUNNING
97
+ except (ImportError, KeyError) as e:
98
+ self.logger.error(f"Failed to check running status: {e}")
99
+ return False
100
+
101
+ def get_host(self, port: int) -> Optional[str]:
102
+ """Get the external host address for a specific port.
103
+
104
+ Args:
105
+ port: Port number to get host for
106
+
107
+ Returns:
108
+ External host address or None if sandbox not available
109
+ """
110
+ try:
111
+ from .utils import _servers
112
+ server = _servers[self.id]
113
+ if server.sandbox is None:
114
+ self.logger.warn('Cannot get host: sandbox not available')
115
+ return None
116
+ return server.sandbox.get_sandbox_host(port)
117
+ except (ImportError, KeyError) as e:
118
+ self.logger.error(f"Failed to get host: {e}")
119
+ return None
120
+
121
+ async def close(self) -> None:
122
+ """Close the session and cleanup resources."""
123
+ # Set closed flag to prevent new keep-alive cycles
124
+ self._is_closed = True
125
+
126
+ # Close WebSocket connection with timeout
127
+ if self._ws and self._ws.close_code is None:
128
+ try:
129
+ # Add timeout to prevent hanging on close
130
+ await asyncio.wait_for(self._ws.close(), timeout=5.0)
131
+ self.logger.debug('✅ WebSocket closed successfully')
132
+ except asyncio.TimeoutError:
133
+ self.logger.debug('⚠️ WebSocket close timeout, forcing termination')
134
+ # Force close if timeout
135
+ if hasattr(self._ws, 'transport') and self._ws.transport:
136
+ self._ws.transport.close()
137
+ except Exception as e:
138
+ self.logger.error(f"Failed to close WebSocket connection: {e}")
139
+
140
+ # Cleanup server resources
141
+ try:
142
+ from .utils import _servers
143
+ await _servers[self.id].cleanup()
144
+ except (ImportError, KeyError) as e:
145
+ # In test environment or when server is not available, skip cleanup
146
+ self.logger.error(f"Warning: Server cleanup not available: {e}")
@@ -23,6 +23,7 @@ class ISandboxConfig(TypedDict):
23
23
  timeout: int # Required: Default timeout in milliseconds
24
24
  workspace: NotRequired[str] # Optional: Grasp workspace ID
25
25
  debug: NotRequired[bool] # Optional: Enable debug mode for detailed logging
26
+ keepAliveMs: NotRequired[int] # Optional: Keep-alive time in milliseconds
26
27
 
27
28
 
28
29
  class IBrowserConfig(TypedDict):
@@ -35,19 +36,23 @@ class IBrowserConfig(TypedDict):
35
36
 
36
37
  class ICommandOptions(TypedDict):
37
38
  """Command execution options interface."""
38
- inBackground: NotRequired[bool] # Whether to run command in background
39
- timeout: NotRequired[int] # Timeout in milliseconds
39
+ inBackground: NotRequired[bool] # Whether to run command in background (corresponds to 'background' in TypeScript)
40
+ timeout_ms: NotRequired[int] # Timeout in milliseconds (corresponds to 'timeoutMs' in TypeScript)
40
41
  cwd: NotRequired[str] # Working directory
41
- nohup: NotRequired[bool] # Whether to use nohup for command execution
42
+ user: NotRequired[str] # User to run the command as (default: 'user')
43
+ envs: NotRequired[Dict[str, str]] # Environment variables for the command
44
+ nohup: NotRequired[bool] # Whether to use nohup for command execution (Grasp-specific extension)
45
+ # Note: onStdout and onStderr callbacks are handled differently in Python implementation
42
46
 
43
47
 
44
48
  class IScriptOptions(TypedDict):
45
49
  """Script execution options interface."""
46
50
  type: str # Required: Script type: 'cjs' for CommonJS, 'esm' for ES Modules, 'py' for Python
47
51
  cwd: NotRequired[str] # Working directory
48
- timeoutMs: NotRequired[int] # Timeout in milliseconds
52
+ timeout_ms: NotRequired[int] # Timeout in milliseconds
49
53
  background: NotRequired[bool] # Run in background
50
- nohup: NotRequired[bool] # Use nohup for background execution
54
+ user: NotRequired[str] # User to run the script as (default: 'user')
55
+ nohup: NotRequired[bool] # Use nohup for background execution (Grasp-specific extension)
51
56
  envs: NotRequired[Dict[str, str]] # The environment variables
52
57
  preCommand: NotRequired[str] # Pre command
53
58
 
@@ -106,6 +106,7 @@ class BrowserService:
106
106
  'HEADLESS': str(self.config['headless']).lower(),
107
107
  'NODE_ENV': 'production',
108
108
  # 'SANDBOX_ID': self.sandbox_service.id,
109
+ 'PLAYWRIGHT_BROWSERS_PATH': '0',
109
110
  'WORKSPACE': self.sandbox_service.workspace,
110
111
  'BROWSER_TYPE': browser_type,
111
112
  **self.config['envs']
@@ -185,7 +186,7 @@ class BrowserService:
185
186
  'type': 'esm',
186
187
  'background': True,
187
188
  'nohup': not self.sandbox_service.is_debug,
188
- 'timeoutMs': 0,
189
+ 'timeout_ms': 0,
189
190
  'preCommand': ''
190
191
  }
191
192
 
@@ -376,8 +377,9 @@ class BrowserService:
376
377
  self._health_check_task = None
377
378
 
378
379
  # Kill the browser process
379
- # if hasattr(self.browser_process, 'kill'):
380
- # await self.browser_process.kill()
380
+ if self.browser_process is not None:
381
+ if hasattr(self.browser_process, 'kill'):
382
+ await self.browser_process.kill()
381
383
 
382
384
  self.browser_process = None
383
385
  self.cdp_connection = None
@@ -91,4 +91,33 @@ class FileSystemService:
91
91
  File content as string or bytes depending on encoding
92
92
  """
93
93
  read_result = await self.sandbox.read_file_from_sandbox(remote_path, options)
94
- return read_result
94
+ return read_result
95
+
96
+ async def sync_downloads_directory(
97
+ self,
98
+ local_path: str,
99
+ remote_path: Optional[str] = None
100
+ ) -> str:
101
+ """Synchronize downloads directory from sandbox to local filesystem.
102
+
103
+ This method is experimental and may change or be removed in future versions.
104
+ Use with caution in production environments.
105
+
106
+ Args:
107
+ local_path: Local directory path to sync to
108
+ remote_path: Remote directory path to sync from (defaults to local_path)
109
+
110
+ Returns:
111
+ Local sync directory path
112
+
113
+ Raises:
114
+ RuntimeError: If sandbox is not running or sync fails
115
+ """
116
+ if remote_path is None:
117
+ remote_path = local_path
118
+
119
+ sync_result = await self.sandbox.sync_downloads_directory(
120
+ dist=local_path,
121
+ src=remote_path
122
+ )
123
+ return sync_result
@@ -42,6 +42,8 @@ class CommandEventEmitter:
42
42
  def set_handle(self, handle: Any) -> None:
43
43
  """Set the command handle (used internally)."""
44
44
  self.handle = handle
45
+ if self.is_killed:
46
+ self.handle.kill()
45
47
 
46
48
  def on(self, event: str, callback: Callable) -> None:
47
49
  """Register event listener."""
@@ -98,9 +100,9 @@ class CommandEventEmitter:
98
100
 
99
101
  async def kill(self) -> None:
100
102
  """Kill the running command."""
101
- if not self.handle:
102
- raise RuntimeError('Command handle not set')
103
- await self.handle.kill()
103
+ self.is_killed = True
104
+ if self.handle:
105
+ await self.handle.kill()
104
106
 
105
107
  def get_handle(self) -> Optional[Any]:
106
108
  """Get the original command handle."""
@@ -284,10 +286,11 @@ class SandboxService:
284
286
  options = {}
285
287
 
286
288
  cwd = options.get('cwd', self.DEFAULT_WORKING_DIRECTORY)
287
- timeout_ms = options.get('timeout', self.config['timeout'])
289
+ timeout_ms = options.get('timeout_ms', 0)
288
290
  use_nohup = options.get('nohup', False)
289
291
  in_background = options.get('inBackground', False)
290
292
  envs = options.get('envs', {})
293
+ user = options.get('user', 'user')
291
294
 
292
295
  # print(f'command: {command}')
293
296
  # print(f'🚀 options {options}')
@@ -316,9 +319,10 @@ class SandboxService:
316
319
  result = await self.sandbox.commands.run(
317
320
  final_command,
318
321
  cwd=cwd,
319
- timeout=timeout_ms,
322
+ timeout=timeout_ms // 1000,
320
323
  envs=envs,
321
324
  background=in_background,
325
+ user=user,
322
326
  )
323
327
  else:
324
328
  raise RuntimeError('Sandbox commands interface not available')
@@ -344,6 +348,7 @@ class SandboxService:
344
348
  timeout=timeout_ms // 1000,
345
349
  envs=envs,
346
350
  background=in_background,
351
+ user=user,
347
352
  )
348
353
  return None
349
354
 
@@ -363,6 +368,7 @@ class SandboxService:
363
368
  timeout=timeout_ms // 1000,
364
369
  background=True,
365
370
  envs=envs,
371
+ user=user,
366
372
  on_stdout=lambda data: event_emitter.emit('stdout', data),
367
373
  on_stderr=lambda data: event_emitter.emit('stderr', data)
368
374
  )
@@ -467,10 +473,11 @@ class SandboxService:
467
473
  # Prepare command options
468
474
  cmd_options: Optional[Any] = {
469
475
  'cwd': options.get('cwd'),
470
- 'timeout': options.get('timeoutMs'),
476
+ 'timeout_ms': options.get('timeout_ms', 0),
471
477
  'inBackground': options.get('background', False),
472
478
  'nohup': options.get('nohup', False),
473
479
  'envs': envs,
480
+ 'user': options.get('user', 'user'),
474
481
  }
475
482
 
476
483
  # Execute the script
@@ -858,7 +865,7 @@ class SandboxService:
858
865
  raise ValueError(error_msg)
859
866
 
860
867
  remote_path = src
861
- local_path = os.path.join(dist, str(self.id), str(int(time.time() * 1000)))
868
+ local_path = dist
862
869
 
863
870
  try:
864
871
  self.logger.debug('🗂️ Starting recursive directory sync', {
@@ -138,6 +138,11 @@ class Commands:
138
138
  # Force background execution for streaming
139
139
  bg_options: ICommandOptions = {**options, 'inBackground': True, 'nohup': False}
140
140
 
141
+ # Set browser endpoint in environment variables
142
+ if 'envs' not in bg_options:
143
+ bg_options['envs'] = {}
144
+ bg_options['envs']['BROWSER_ENDPOINT'] = self.connection.ws_url
145
+
141
146
  emitter: Optional[CommandEventEmitter] = await self.sandbox.run_command(
142
147
  command, bg_options
143
148
  )
@@ -4,7 +4,7 @@ This package contains utility functions and classes for configuration,
4
4
  logging, authentication, and other common functionality."""
5
5
 
6
6
  # Import all utility modules
7
- from .config import get_config, get_sandbox_config, get_browser_config, DEFAULT_CONFIG
7
+ from .config import get_config, get_sandbox_config, DEFAULT_CONFIG
8
8
  from .logger import Logger, init_logger, get_logger, debug, info, warn, error
9
9
  from .auth import verify, login, AuthError, KeyVerificationError, AuthManager
10
10
 
@@ -12,7 +12,6 @@ __all__ = [
12
12
  # Configuration
13
13
  'get_config',
14
14
  'get_sandbox_config',
15
- 'get_browser_config',
16
15
  'DEFAULT_CONFIG',
17
16
  # Logging
18
17
  'Logger',
@@ -25,9 +25,10 @@ def get_config() -> Dict[str, Any]:
25
25
  'key': os.getenv('GRASP_KEY', ''),
26
26
  'timeout': int(os.getenv('GRASP_SERVICE_TIMEOUT', '900000')),
27
27
  'debug': os.getenv('GRASP_DEBUG', 'false').lower() == 'true',
28
+ 'keepAliveMs': int(os.getenv('GRASP_KEEP_ALIVE_MS', '10000')),
28
29
  },
29
30
  'logger': {
30
- 'level': os.getenv('GRASP_LOG_LEVEL', 'info'),
31
+ 'level': os.getenv('GRASP_LOG_LEVEL', None),
31
32
  'console': True,
32
33
  'file': os.getenv('GRASP_LOG_FILE'),
33
34
  },
@@ -54,40 +55,9 @@ def get_sandbox_config() -> ISandboxConfig:
54
55
 
55
56
  return sandbox_config
56
57
 
57
-
58
- def get_browser_config() -> IBrowserConfig:
59
- """Gets browser-specific configuration.
60
-
61
- Returns:
62
- IBrowserConfig: Browser configuration object.
63
- """
64
- return IBrowserConfig(
65
- args=[
66
- '--no-sandbox',
67
- '--disable-setuid-sandbox',
68
- '--disable-dev-shm-usage',
69
- '--disable-gpu',
70
- '--remote-debugging-port=9222',
71
- '--remote-debugging-address=0.0.0.0',
72
- ],
73
- headless=os.getenv('GRASP_HEADLESS', 'true').lower() == 'true',
74
- launchTimeout=int(os.getenv('GRASP_LAUNCH_TIMEOUT', '30000')),
75
- envs={
76
- 'PLAYWRIGHT_BROWSERS_PATH': '0',
77
- 'DISPLAY': ':99',
78
- },
79
- )
80
-
81
-
82
58
  # Default configuration constants
83
59
  DEFAULT_CONFIG = {
84
- 'PLAYWRIGHT_BROWSERS_PATH': '0',
85
60
  'WORKING_DIRECTORY': '/home/user',
86
- 'SCREENSHOT_PATH': '/home/user',
87
- 'DEFAULT_VIEWPORT': {
88
- 'width': 1280,
89
- 'height': 720,
90
- },
91
61
  }
92
62
 
93
63
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: grasp_sdk
3
- Version: 0.2.0a1
3
+ Version: 0.2.0b2
4
4
  Summary: Python SDK for Grasp E2B - Browser automation and sandbox management
5
5
  Home-page: https://github.com/grasp-team/grasp-e2b
6
6
  Author: Grasp Team
@@ -15,11 +15,9 @@ setup.py
15
15
  ./examples/grasp_terminal.py
16
16
  ./examples/grasp_usage.py
17
17
  ./examples/test_async_context.py
18
- ./examples/test_get_replay_screenshots.py
19
18
  ./examples/test_grasp_classes.py
20
19
  ./examples/test_python_script.py
21
20
  ./examples/test_removed_methods.py
22
- ./examples/test_shutdown_deprecation.py
23
21
  ./examples/test_terminal_updates.py
24
22
  ./grasp_sdk/__init__.py
25
23
  ./grasp_sdk/grasp/__init__.py
@@ -27,7 +25,6 @@ setup.py
27
25
  ./grasp_sdk/grasp/index.py
28
26
  ./grasp_sdk/grasp/server.py
29
27
  ./grasp_sdk/grasp/session.py
30
- ./grasp_sdk/grasp/terminal.py
31
28
  ./grasp_sdk/grasp/utils.py
32
29
  ./grasp_sdk/models/__init__.py
33
30
  ./grasp_sdk/services/__init__.py
@@ -46,11 +43,9 @@ examples/example_readfile_usage.py
46
43
  examples/grasp_terminal.py
47
44
  examples/grasp_usage.py
48
45
  examples/test_async_context.py
49
- examples/test_get_replay_screenshots.py
50
46
  examples/test_grasp_classes.py
51
47
  examples/test_python_script.py
52
48
  examples/test_removed_methods.py
53
- examples/test_shutdown_deprecation.py
54
49
  examples/test_terminal_updates.py
55
50
  grasp_sdk/__init__.py
56
51
  grasp_sdk.egg-info/PKG-INFO
@@ -65,7 +60,6 @@ grasp_sdk/grasp/browser.py
65
60
  grasp_sdk/grasp/index.py
66
61
  grasp_sdk/grasp/server.py
67
62
  grasp_sdk/grasp/session.py
68
- grasp_sdk/grasp/terminal.py
69
63
  grasp_sdk/grasp/utils.py
70
64
  grasp_sdk/models/__init__.py
71
65
  grasp_sdk/services/__init__.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "grasp_sdk"
7
- version = "0.2.0a1"
7
+ version = "0.2.0b2"
8
8
  authors = [
9
9
  {name = "Grasp Team", email = "team@grasp.com"},
10
10
  ]
@@ -1,90 +0,0 @@
1
- #!/usr/bin/env python3
2
- """测试 get_replay_screenshots 功能的示例脚本
3
-
4
- 这个脚本演示如何使用 get_replay_screenshots 函数获取截图目录路径。
5
- """
6
-
7
- import asyncio
8
- import os
9
- from grasp_sdk import launch_browser, get_replay_screenshots
10
-
11
-
12
- async def test_get_replay_screenshots():
13
- """测试 get_replay_screenshots 功能"""
14
- print("🚀 启动浏览器服务...")
15
-
16
- try:
17
- # 启动浏览器并获取连接信息
18
- connection = await launch_browser({
19
- 'headless': True,
20
- 'type': 'chromium'
21
- })
22
-
23
- print(f"✅ 浏览器启动成功!")
24
- print(f"连接 ID: {connection.id}")
25
- print(f"WebSocket URL: {connection.ws_url or connection.ws_url}")
26
- print(f"HTTP URL: {connection.http_url}")
27
-
28
- # 获取截图目录路径
29
- print("\n📸 获取截图目录路径...")
30
- screenshots_dir = await get_replay_screenshots(connection)
31
-
32
- print(f"✅ 截图目录路径: {screenshots_dir}")
33
-
34
- # 检查目录是否存在
35
- if os.path.exists(screenshots_dir):
36
- print(f"📁 目录已存在")
37
-
38
- # 列出目录中的文件
39
- files = os.listdir(screenshots_dir)
40
- if files:
41
- print(f"📋 目录中有 {len(files)} 个文件:")
42
- for file in files[:5]: # 只显示前5个文件
43
- print(f" - {file}")
44
- if len(files) > 5:
45
- print(f" ... 还有 {len(files) - 5} 个文件")
46
- else:
47
- print("📂 目录为空")
48
- else:
49
- print(f"⚠️ 目录不存在,将在首次使用时创建")
50
-
51
- # 测试错误处理
52
- print("\n🧪 测试错误处理...")
53
-
54
- # 测试缺少 id 字段的情况
55
- try:
56
- invalid_connection = {
57
- 'ws_url': connection.ws_url,
58
- 'http_url': connection.http_url
59
- # 缺少 'id' 字段
60
- }
61
- await get_replay_screenshots(invalid_connection)
62
- except ValueError as e:
63
- print(f"✅ 正确捕获 ValueError: {e}")
64
-
65
- # 测试无效 ID 的情况
66
- try:
67
- invalid_connection = {
68
- 'id': 'invalid-id-123',
69
- 'ws_url': connection.ws_url,
70
- 'http_url': connection.http_url
71
- }
72
- await get_replay_screenshots(invalid_connection)
73
- except KeyError as e:
74
- print(f"✅ 正确捕获 KeyError: {e}")
75
-
76
- print("\n🎉 所有测试完成!")
77
-
78
- except Exception as e:
79
- print(f"❌ 测试失败: {e}")
80
- raise
81
-
82
-
83
- if __name__ == "__main__":
84
- # 检查环境变量
85
- if not os.getenv('GRASP_KEY'):
86
- print("⚠️ 警告: 未设置 GRASP_KEY 环境变量")
87
- print("请设置: export GRASP_KEY=your_api_key")
88
-
89
- # 运行测试
90
- asyncio.run(test_get_replay_screenshots())
@@ -1,62 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- 测试 shutdown 方法的废弃警告
4
- """
5
-
6
- import warnings
7
- import sys
8
- import os
9
-
10
- # 添加项目路径
11
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
12
-
13
- from grasp_sdk import shutdown
14
-
15
- def test_shutdown_deprecation():
16
- """测试 shutdown 方法是否正确发出废弃警告"""
17
- print("🧪 测试 shutdown 方法的废弃警告...")
18
-
19
- # 设置警告过滤器显示所有警告
20
- warnings.filterwarnings("always")
21
-
22
- # 捕获警告
23
- with warnings.catch_warnings(record=True) as w:
24
- warnings.simplefilter("always")
25
-
26
- print("📞 调用 shutdown(None)...")
27
-
28
- # 调用废弃的 shutdown 方法
29
- try:
30
- shutdown(None)
31
- print("✅ shutdown 方法调用完成")
32
- except SystemExit:
33
- print("⚠️ shutdown 方法触发了系统退出")
34
- except Exception as e:
35
- print(f"⚠️ shutdown 方法调用出现异常: {e}")
36
-
37
- # 检查是否有废弃警告
38
- print(f"\n📊 捕获到 {len(w)} 个警告")
39
- deprecation_warnings = [warning for warning in w if issubclass(warning.category, DeprecationWarning)]
40
-
41
- if deprecation_warnings:
42
- print(f"✅ 检测到 {len(deprecation_warnings)} 个废弃警告:")
43
- for warning in deprecation_warnings:
44
- print(f" - {warning.message}")
45
- print(f" 文件: {warning.filename}:{warning.lineno}")
46
- else:
47
- print("❌ 未检测到废弃警告")
48
- if w:
49
- print("其他警告:")
50
- for warning in w:
51
- print(f" - {warning.category.__name__}: {warning.message}")
52
-
53
- print("\n📋 废弃方法迁移指南:")
54
- print(" 旧方式: shutdown(connection)")
55
- print(" 新方式: await session.close()")
56
- print("\n💡 建议:")
57
- print(" - 使用 grasp.launch() 获取 session")
58
- print(" - 使用 await session.close() 关闭会话")
59
- print(" - 避免直接使用 shutdown() 方法")
60
-
61
- if __name__ == '__main__':
62
- test_shutdown_deprecation()
@@ -1,69 +0,0 @@
1
- """GraspBrowser class for browser interface."""
2
-
3
- import aiohttp
4
- from typing import Optional, Dict, Any
5
-
6
- from ..services.browser import CDPConnection
7
-
8
-
9
- class GraspBrowser:
10
- """Browser interface for Grasp session."""
11
-
12
- def __init__(self, connection: CDPConnection):
13
- """Initialize GraspBrowser.
14
-
15
- Args:
16
- connection: CDP connection instance
17
- """
18
- self.connection = connection
19
-
20
- def get_endpoint(self) -> str:
21
- """Get browser WebSocket endpoint URL.
22
-
23
- Returns:
24
- WebSocket URL for browser connection
25
- """
26
- return self.connection.ws_url
27
-
28
- async def get_current_page_target_info(self) -> Optional[Dict[str, Any]]:
29
- """Get current page target information.
30
-
31
- Warning:
32
- This method is experimental and may change or be removed in future versions.
33
- Use with caution in production environments.
34
-
35
- Returns:
36
- Dictionary containing target info and last screenshot, or None if failed
37
- """
38
- host = self.connection.http_url
39
- api = f"{host}/api/page/info"
40
-
41
- try:
42
- async with aiohttp.ClientSession() as session:
43
- async with session.get(api) as response:
44
- if not response.ok:
45
- return None
46
-
47
- data = await response.json()
48
- page_info = data.get('pageInfo', {})
49
- last_screenshot = page_info.get('lastScreenshot')
50
- session_id = page_info.get('sessionId')
51
- targets = page_info.get('targets', {})
52
-
53
- if session_id and session_id in targets:
54
- target_info = targets[session_id].copy()
55
- target_info['lastScreenshot'] = last_screenshot
56
- return target_info
57
-
58
- return None
59
- except Exception:
60
- return None
61
-
62
- def get_liveview_url(self) -> Optional[str]:
63
- """Get liveview URL (TODO: implementation pending).
64
-
65
- Returns:
66
- Liveview URL or None
67
- """
68
- # TODO: Implement liveview URL generation
69
- return None
@@ -1,108 +0,0 @@
1
- """GraspSession class for session management."""
2
-
3
- import asyncio
4
- import websockets
5
- from .browser import GraspBrowser
6
- from .terminal import GraspTerminal
7
- from ..services.browser import CDPConnection
8
- from ..services.filesystem import FileSystemService
9
-
10
-
11
- class GraspSession:
12
- """Main session interface providing access to browser, terminal, and files."""
13
-
14
- def __init__(self, connection: CDPConnection):
15
- """Initialize GraspSession.
16
-
17
- Args:
18
- connection: CDP connection instance
19
- """
20
- self.connection = connection
21
- self.browser = GraspBrowser(connection)
22
- self.terminal = GraspTerminal(connection)
23
-
24
- # Create files service
25
- connection_id = connection.id
26
- try:
27
- from . import _servers
28
- server = _servers[connection_id]
29
- if server.sandbox is None:
30
- raise RuntimeError('Sandbox is not available for file system service')
31
- self.files = FileSystemService(server.sandbox, connection)
32
- except (ImportError, KeyError) as e:
33
- # In test environment or when server is not available, create a mock files service
34
- print(f"Warning: Files service not available: {e}")
35
- raise e
36
-
37
- # WebSocket connection for keep-alive
38
- self._ws = None
39
- self._keep_alive_task = None
40
-
41
- # Initialize WebSocket connection and start keep-alive
42
- print('create session...')
43
- asyncio.create_task(self._initialize_websocket())
44
-
45
- async def _initialize_websocket(self) -> None:
46
- """Initialize WebSocket connection and start keep-alive."""
47
- try:
48
- self._ws = await websockets.connect(self.connection.ws_url)
49
- await self._keep_alive()
50
- except Exception as e:
51
- print(f"Failed to initialize WebSocket connection: {e}")
52
-
53
- async def _keep_alive(self) -> None:
54
- """Keep WebSocket connection alive with periodic ping."""
55
- if self._ws is None:
56
- return
57
-
58
- async def ping_loop():
59
- while self._ws and self._ws.close_code is None:
60
- try:
61
- await self._ws.ping()
62
- await asyncio.sleep(10) # 10 seconds interval, same as TypeScript version
63
- except Exception as e:
64
- print(f"Keep-alive ping failed: {e}")
65
- break
66
-
67
- self._keep_alive_task = asyncio.create_task(ping_loop())
68
-
69
- @property
70
- def id(self) -> str:
71
- """Get session ID.
72
-
73
- Returns:
74
- Session ID
75
- """
76
- return self.connection.id
77
-
78
- async def close(self) -> None:
79
- """Close the session and cleanup resources."""
80
- # Stop keep-alive task
81
- if self._keep_alive_task:
82
- self._keep_alive_task.cancel()
83
- try:
84
- await self._keep_alive_task
85
- except asyncio.CancelledError:
86
- pass
87
-
88
- # Close WebSocket connection with timeout
89
- if self._ws and self._ws.close_code is None:
90
- try:
91
- # Add timeout to prevent hanging on close
92
- await asyncio.wait_for(self._ws.close(), timeout=5.0)
93
- print('✅ WebSocket closed successfully')
94
- except asyncio.TimeoutError:
95
- print('⚠️ WebSocket close timeout, forcing termination')
96
- # Force close if timeout
97
- if hasattr(self._ws, 'transport') and self._ws.transport:
98
- self._ws.transport.close()
99
- except Exception as e:
100
- print(f"Failed to close WebSocket connection: {e}")
101
-
102
- # Cleanup server resources
103
- try:
104
- from . import _servers
105
- await _servers[self.id].cleanup()
106
- except (ImportError, KeyError) as e:
107
- # In test environment or when server is not available, skip cleanup
108
- print(f"Warning: Server cleanup not available: {e}")
@@ -1,32 +0,0 @@
1
- """GraspTerminal class for terminal interface."""
2
-
3
- from ..services.browser import CDPConnection
4
- from ..services.terminal import TerminalService
5
-
6
-
7
- class GraspTerminal:
8
- """Terminal interface for Grasp session."""
9
-
10
- def __init__(self, connection: CDPConnection):
11
- """Initialize GraspTerminal.
12
-
13
- Args:
14
- connection: CDP connection instance
15
- """
16
- self.connection = connection
17
-
18
- def create(self) -> TerminalService:
19
- """Create a new terminal service instance.
20
-
21
- Returns:
22
- TerminalService instance
23
-
24
- Raises:
25
- RuntimeError: If sandbox is not available
26
- """
27
- connection_id = self.connection.id
28
- from . import _servers
29
- server = _servers[connection_id]
30
- if server.sandbox is None:
31
- raise RuntimeError('Sandbox is not available')
32
- return TerminalService(server.sandbox, self.connection)
File without changes
File without changes
File without changes
File without changes
File without changes