import asyncio import os import shutil import sys import uuid from dataclasses import dataclass from pathlib import Path from typing import Optional from openhands.core.logger import openhands_logger as logger from openhands.events.action import Action from openhands.events.observation import Observation from openhands.runtime.plugins.requirement import Plugin, PluginRequirement from openhands.runtime.utils.system import check_port_available from openhands.utils.shutdown_listener import should_continue @dataclass class VSCodeRequirement(PluginRequirement): name: str = 'vscode' class VSCodePlugin(Plugin): name: str = 'vscode' vscode_port: Optional[int] = None vscode_connection_token: Optional[str] = None gateway_process: asyncio.subprocess.Process async def initialize(self, username: str) -> None: # Check if we're on Windows - VSCode plugin is not supported on Windows if os.name == 'nt' or sys.platform == 'win32': self.vscode_port = None self.vscode_connection_token = None logger.warning( 'VSCode plugin is not supported on Windows. Plugin will be disabled.' ) return if username not in ['root', 'openhands']: self.vscode_port = None self.vscode_connection_token = None logger.warning( 'VSCodePlugin is only supported for root or openhands user. ' 'It is not yet supported for other users (i.e., when running LocalRuntime).' ) return # Set up VSCode settings.json self._setup_vscode_settings() try: self.vscode_port = int(os.environ['VSCODE_PORT']) except (KeyError, ValueError): logger.warning( 'VSCODE_PORT environment variable not set or invalid. VSCode plugin will be disabled.' ) return self.vscode_connection_token = str(uuid.uuid4()) if not check_port_available(self.vscode_port): logger.warning( f'Port {self.vscode_port} is not available. VSCode plugin will be disabled.' ) return cmd = ( f"su - {username} -s /bin/bash << 'EOF'\n" f'sudo chown -R {username}:{username} /openhands/.openvscode-server\n' 'cd /workspace\n' f'exec /openhands/.openvscode-server/bin/openvscode-server --host 0.0.0.0 --connection-token {self.vscode_connection_token} --port {self.vscode_port} --disable-workspace-trust\n' 'EOF' ) # Using asyncio.create_subprocess_shell instead of subprocess.Popen # to avoid ASYNC101 linting error self.gateway_process = await asyncio.create_subprocess_shell( cmd, stderr=asyncio.subprocess.STDOUT, stdout=asyncio.subprocess.PIPE, ) # read stdout until the kernel gateway is ready output = '' while should_continue() and self.gateway_process.stdout is not None: line_bytes = await self.gateway_process.stdout.readline() line = line_bytes.decode('utf-8') print(line) output += line if 'at' in line: break await asyncio.sleep(1) logger.debug('Waiting for VSCode server to start...') logger.debug( f'VSCode server started at port {self.vscode_port}. Output: {output}' ) def _setup_vscode_settings(self) -> None: """ Set up VSCode settings by creating the .vscode directory in the workspace and copying the settings.json file there. """ # Get the path to the settings.json file in the plugin directory current_dir = Path(__file__).parent settings_path = current_dir / 'settings.json' # Create the .vscode directory in the workspace if it doesn't exist vscode_dir = Path('/workspace/.vscode') vscode_dir.mkdir(parents=True, exist_ok=True) # Copy the settings.json file to the .vscode directory target_path = vscode_dir / 'settings.json' shutil.copy(settings_path, target_path) # Make sure the settings file is readable and writable by all users os.chmod(target_path, 0o666) logger.debug(f'VSCode settings copied to {target_path}') async def run(self, action: Action) -> Observation: """Run the plugin for a given action.""" raise NotImplementedError('VSCodePlugin does not support run method')