|
5 | 5 | import gzip |
6 | 6 | import json |
7 | 7 | import os |
8 | | -import pwd |
| 8 | +try: |
| 9 | + import pwd |
| 10 | +except ImportError: |
| 11 | + pwd = None |
9 | 12 | import random |
10 | | -import resource |
| 13 | +try: |
| 14 | + import resource |
| 15 | +except ImportError: |
| 16 | + resource = None |
11 | 17 | import socket |
12 | 18 | import subprocess |
13 | 19 | import sys |
|
24 | 30 | from maxkb.const import BASE_DIR, CONFIG |
25 | 31 | from maxkb.const import PROJECT_DIR |
26 | 32 |
|
| 33 | +IS_WINDOWS = sys.platform.startswith('win') |
| 34 | + |
27 | 35 | _enable_sandbox = bool(int(CONFIG.get('SANDBOX', 0))) |
28 | 36 | _run_user = 'sandbox' if _enable_sandbox else getpass.getuser() |
29 | 37 | _sandbox_path = CONFIG.get("SANDBOX_HOME", '/opt/maxkb-app/sandbox') if _enable_sandbox else os.path.join(PROJECT_DIR, 'data', 'sandbox') |
@@ -89,7 +97,10 @@ def init_sandbox_dir(): |
89 | 97 | def exec_code(self, code_str, keywords, function_name=None): |
90 | 98 | _id = str(uuid.uuid7()) |
91 | 99 | action_function = f'({function_name !a}, locals_v.get({function_name !a}))' if function_name else 'locals_v.popitem()' |
92 | | - set_run_user = f'os.setgid({pwd.getpwnam(_run_user).pw_gid});os.setuid({pwd.getpwnam(_run_user).pw_uid});' if _enable_sandbox else '' |
| 100 | + if pwd and _enable_sandbox: |
| 101 | + set_run_user = f'os.setgid({pwd.getpwnam(_run_user).pw_gid});os.setuid({pwd.getpwnam(_run_user).pw_uid});' |
| 102 | + else: |
| 103 | + set_run_user = '' |
93 | 104 | _exec_code = f""" |
94 | 105 | try: |
95 | 106 | import os, sys, json |
@@ -118,11 +129,31 @@ def exec_code(self, code_str, keywords, function_name=None): |
118 | 129 | sys.stdout.flush() |
119 | 130 | """ |
120 | 131 | maxkb_logger.debug(f"Tool execution({_id}) execute code: {_exec_code}") |
121 | | - with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=True) as f: |
122 | | - f.write(_exec_code) |
123 | | - f.flush() |
124 | | - with execution_timer(_id): |
125 | | - subprocess_result = self._exec(f.name, _id) |
| 132 | + |
| 133 | + # Windows 需要特殊处理临时文件 |
| 134 | + if IS_WINDOWS: |
| 135 | + # 在 Windows 上,创建临时文件后需要关闭才能被 subprocess 访问 |
| 136 | + temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) |
| 137 | + try: |
| 138 | + temp_file.write(_exec_code) |
| 139 | + temp_file.flush() |
| 140 | + temp_file.close() # 关闭文件以便 subprocess 可以访问 |
| 141 | + with execution_timer(_id): |
| 142 | + subprocess_result = self._exec(temp_file.name, _id) |
| 143 | + finally: |
| 144 | + # 执行完成后删除临时文件 |
| 145 | + try: |
| 146 | + os.unlink(temp_file.name) |
| 147 | + except: |
| 148 | + pass |
| 149 | + else: |
| 150 | + # 其他环境,直接执行 |
| 151 | + with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=True) as f: |
| 152 | + f.write(_exec_code) |
| 153 | + f.flush() |
| 154 | + with execution_timer(_id): |
| 155 | + subprocess_result = self._exec(f.name, _id) |
| 156 | + |
126 | 157 | if subprocess_result.returncode != 0: |
127 | 158 | raise Exception(subprocess_result.stderr or subprocess_result.stdout or "Unknown exception occurred") |
128 | 159 | lines = subprocess_result.stdout.splitlines() |
@@ -265,7 +296,10 @@ def visit_Return(self, node): |
265 | 296 |
|
266 | 297 | def generate_mcp_server_code(self, code_str, params, name, description, tool_id): |
267 | 298 | code = self._generate_mcp_server_code(code_str, params, name, description, tool_id) |
268 | | - set_run_user = f'os.setgid({pwd.getpwnam(_run_user).pw_gid});os.setuid({pwd.getpwnam(_run_user).pw_uid});' if _enable_sandbox else '' |
| 299 | + if pwd and _enable_sandbox: |
| 300 | + set_run_user = f'os.setgid({pwd.getpwnam(_run_user).pw_gid});os.setuid({pwd.getpwnam(_run_user).pw_uid});' |
| 301 | + else: |
| 302 | + set_run_user = '' |
269 | 303 | return f""" |
270 | 304 | import os, sys, logging |
271 | 305 | logging.basicConfig(level=logging.WARNING) |
@@ -313,18 +347,32 @@ def _exec(self, execute_file, _id): |
313 | 347 | '_ID': _id, |
314 | 348 | }} |
315 | 349 | def _set_resource_limit(): |
316 | | - if not _enable_sandbox or not sys.platform.startswith("linux"): return |
317 | | - with suppress(Exception): resource.setrlimit(resource.RLIMIT_AS, (_process_limit_mem_mb * 1024 * 1024,) * 2) |
318 | | - with suppress(Exception): os.sched_setaffinity(0, set(random.sample(list(os.sched_getaffinity(0)), _process_limit_cpu_cores))) |
| 350 | + if not _enable_sandbox or IS_WINDOWS or not resource: |
| 351 | + return |
| 352 | + with suppress(Exception): |
| 353 | + resource.setrlimit(resource.RLIMIT_AS, (_process_limit_mem_mb * 1024 * 1024,) * 2) |
| 354 | + with suppress(Exception): |
| 355 | + if hasattr(os, 'sched_setaffinity') and hasattr(os, 'sched_getaffinity'): |
| 356 | + os.sched_setaffinity(0, set(random.sample(list(os.sched_getaffinity(0)), _process_limit_cpu_cores))) |
319 | 357 | try: |
320 | | - subprocess_result = subprocess.run( |
321 | | - [sys.executable, execute_file], |
322 | | - timeout=_process_limit_timeout_seconds, |
323 | | - text=True, |
324 | | - capture_output=True, |
325 | | - **kwargs, |
326 | | - preexec_fn=_set_resource_limit |
327 | | - ) |
| 358 | + # Windows 不支持 preexec_fn 参数 |
| 359 | + if IS_WINDOWS: |
| 360 | + subprocess_result = subprocess.run( |
| 361 | + [sys.executable, execute_file], |
| 362 | + timeout=_process_limit_timeout_seconds, |
| 363 | + text=True, |
| 364 | + capture_output=True, |
| 365 | + **kwargs |
| 366 | + ) |
| 367 | + else: |
| 368 | + subprocess_result = subprocess.run( |
| 369 | + [sys.executable, execute_file], |
| 370 | + timeout=_process_limit_timeout_seconds, |
| 371 | + text=True, |
| 372 | + capture_output=True, |
| 373 | + **kwargs, |
| 374 | + preexec_fn=_set_resource_limit |
| 375 | + ) |
328 | 376 | return subprocess_result |
329 | 377 | except subprocess.TimeoutExpired: |
330 | 378 | raise Exception(_(f"Process execution timed out after {_process_limit_timeout_seconds} seconds.")) |
|
0 commit comments