mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: add automatic Chrome dependency installation and Docker browser support for Codespaces
- Implement automatic detection and installation of Chrome dependencies - Add optional Docker-based browser isolation with configurable settings - Create comprehensive devcontainer configuration for GitHub Codespaces - Add environment detection for Codespaces, containers, and Linux systems - Implement fallback mechanisms: system Chrome -> Docker -> downloaded Chrome - Add new VS Code settings for Docker browser configuration - Include post-create and post-start scripts for Codespaces setup - Add comprehensive tests for browser environment functionality Fixes #7990
This commit is contained in:
parent
9d33c109c9
commit
dbe8a5f13d
11 changed files with 1263 additions and 12 deletions
133
.devcontainer/README.md
Normal file
133
.devcontainer/README.md
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
# Roo Code GitHub Codespaces Configuration
|
||||
|
||||
This directory contains the configuration for running Roo Code in GitHub Codespaces, providing a fully configured development environment with automatic Chrome/Puppeteer dependency installation.
|
||||
|
||||
## Features
|
||||
|
||||
### Automatic Dependency Installation
|
||||
|
||||
- **Chrome Browser**: Google Chrome Stable is automatically installed with all required dependencies
|
||||
- **Docker Support**: Docker-in-Docker feature enables containerized browser option
|
||||
- **Node.js Environment**: Pre-configured TypeScript/Node.js development container
|
||||
- **VS Code Extensions**: Essential extensions are pre-installed
|
||||
|
||||
### Browser Tool Support
|
||||
|
||||
The configuration ensures the browser tool works seamlessly in Codespaces by:
|
||||
|
||||
1. **Automatic Chrome Installation**: Chrome and all its dependencies are installed during container creation
|
||||
2. **Dependency Detection**: The extension automatically detects and installs missing dependencies
|
||||
3. **Docker Fallback**: If Chrome fails, Docker browser container can be used as fallback
|
||||
4. **Environment Variables**: Proper configuration for Puppeteer to use system Chrome
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Docker Browser (Optional)
|
||||
|
||||
You can enable Docker-based browser isolation in VS Code settings:
|
||||
|
||||
```json
|
||||
{
|
||||
"roo-cline.browserDocker.enabled": true,
|
||||
"roo-cline.browserDocker.image": "browserless/chrome:latest",
|
||||
"roo-cline.browserDocker.autoStart": true
|
||||
}
|
||||
```
|
||||
|
||||
### Benefits of Docker Browser
|
||||
|
||||
- **Isolation**: Browser runs in a separate container
|
||||
- **Consistency**: Same browser environment across all systems
|
||||
- **No Dependencies**: No need to install Chrome dependencies on host
|
||||
- **Security**: Enhanced security through containerization
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Browser Tool Not Working?
|
||||
|
||||
1. **Check Chrome Installation**:
|
||||
|
||||
```bash
|
||||
google-chrome --version
|
||||
# or
|
||||
google-chrome-stable --version
|
||||
```
|
||||
|
||||
2. **Test Chrome Headless**:
|
||||
|
||||
```bash
|
||||
google-chrome --headless --no-sandbox --disable-gpu --dump-dom https://example.com
|
||||
```
|
||||
|
||||
3. **Check Missing Dependencies**:
|
||||
|
||||
```bash
|
||||
ldd $(which google-chrome) | grep "not found"
|
||||
```
|
||||
|
||||
4. **Install Missing Dependencies Manually**:
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 \
|
||||
libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 \
|
||||
libxrandr2 libgbm1 libasound2
|
||||
```
|
||||
|
||||
5. **Use Docker Browser as Fallback**:
|
||||
- Enable Docker browser in settings (see Configuration Options above)
|
||||
- Ensure Docker is running: `docker info`
|
||||
- The extension will automatically use Docker if Chrome fails
|
||||
|
||||
### Docker Issues?
|
||||
|
||||
1. **Check Docker Status**:
|
||||
|
||||
```bash
|
||||
docker info
|
||||
```
|
||||
|
||||
2. **Pull Browser Image Manually**:
|
||||
|
||||
```bash
|
||||
docker pull browserless/chrome:latest
|
||||
```
|
||||
|
||||
3. **Test Docker Browser**:
|
||||
```bash
|
||||
docker run -d --name test-browser -p 3000:3000 browserless/chrome:latest
|
||||
# Visit http://localhost:3000 to verify
|
||||
docker stop test-browser && docker rm test-browser
|
||||
```
|
||||
|
||||
## Files in This Directory
|
||||
|
||||
- **devcontainer.json**: Main configuration file for the dev container
|
||||
- **post-create.sh**: Script that runs after container creation (installs dependencies)
|
||||
- **post-start.sh**: Script that runs each time the container starts (verifies setup)
|
||||
- **README.md**: This documentation file
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Container Creation**: When you create a Codespace, it uses the TypeScript/Node.js base image
|
||||
2. **Feature Installation**: Docker-in-Docker and Chrome features are installed
|
||||
3. **Post-Create Script**: Installs Chrome dependencies and builds the project
|
||||
4. **Post-Start Script**: Verifies the environment on each container start
|
||||
5. **Automatic Detection**: The extension detects the Codespace environment and adapts accordingly
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The following environment variables are set automatically:
|
||||
|
||||
- `CODESPACES=true`: Indicates running in GitHub Codespaces
|
||||
- `PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true`: Prevents Puppeteer from downloading Chromium
|
||||
- `PUPPETEER_EXECUTABLE_PATH=/usr/bin/google-chrome-stable`: Points Puppeteer to system Chrome
|
||||
|
||||
## Support
|
||||
|
||||
If you encounter issues with the browser tool in Codespaces:
|
||||
|
||||
1. Check the troubleshooting section above
|
||||
2. Review the post-start script output for warnings
|
||||
3. Report issues at: https://github.com/RooCodeInc/Roo-Code/issues
|
||||
70
.devcontainer/devcontainer.json
Normal file
70
.devcontainer/devcontainer.json
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
{
|
||||
"name": "Roo Code Development",
|
||||
"image": "mcr.microsoft.com/devcontainers/typescript-node:20",
|
||||
|
||||
// Features to add to the dev container
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {},
|
||||
"ghcr.io/devcontainers/features/chrome:1": {
|
||||
"version": "stable"
|
||||
}
|
||||
},
|
||||
|
||||
// Configure tool-specific properties
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"esbenp.prettier-vscode",
|
||||
"ms-vscode.vscode-typescript-next",
|
||||
"RooVeterinaryInc.roo-cline"
|
||||
],
|
||||
"settings": {
|
||||
"terminal.integrated.defaultProfile.linux": "bash",
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
|
||||
"editor.formatOnSave": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally
|
||||
"forwardPorts": [3000, 9222],
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created
|
||||
"postCreateCommand": "bash .devcontainer/post-create.sh",
|
||||
|
||||
// Use 'postStartCommand' to run commands after the container starts
|
||||
"postStartCommand": "bash .devcontainer/post-start.sh",
|
||||
|
||||
// Set environment variables
|
||||
"containerEnv": {
|
||||
"CODESPACES": "true",
|
||||
"PUPPETEER_SKIP_CHROMIUM_DOWNLOAD": "true",
|
||||
"PUPPETEER_EXECUTABLE_PATH": "/usr/bin/google-chrome-stable"
|
||||
},
|
||||
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root
|
||||
// "remoteUser": "root",
|
||||
|
||||
// Features documentation: https://containers.dev/features
|
||||
"mounts": ["source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind"],
|
||||
|
||||
// Memory and CPU limits for better performance
|
||||
"hostRequirements": {
|
||||
"cpus": 2,
|
||||
"memory": "4gb",
|
||||
"storage": "32gb"
|
||||
}
|
||||
}
|
||||
77
.devcontainer/post-create.sh
Normal file
77
.devcontainer/post-create.sh
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "🚀 Setting up Roo Code development environment..."
|
||||
|
||||
# Update package lists
|
||||
echo "📦 Updating package lists..."
|
||||
sudo apt-get update
|
||||
|
||||
# Install Chrome dependencies that might be missing
|
||||
echo "🌐 Installing Chrome dependencies..."
|
||||
sudo apt-get install -y \
|
||||
libatk1.0-0 \
|
||||
libatk-bridge2.0-0 \
|
||||
libcups2 \
|
||||
libdrm2 \
|
||||
libxkbcommon0 \
|
||||
libxcomposite1 \
|
||||
libxdamage1 \
|
||||
libxfixes3 \
|
||||
libxrandr2 \
|
||||
libgbm1 \
|
||||
libasound2 \
|
||||
libatspi2.0-0 \
|
||||
libgtk-3-0 \
|
||||
libpango-1.0-0 \
|
||||
libcairo2 \
|
||||
libxshmfence1 \
|
||||
libnss3 \
|
||||
libnssutil3 \
|
||||
libnspr4 \
|
||||
libx11-xcb1 \
|
||||
libxcb-dri3-0 \
|
||||
fonts-liberation \
|
||||
libappindicator3-1 \
|
||||
libxss1 \
|
||||
lsb-release \
|
||||
xdg-utils \
|
||||
wget
|
||||
|
||||
# Verify Chrome installation
|
||||
if ! command -v google-chrome &> /dev/null && ! command -v google-chrome-stable &> /dev/null; then
|
||||
echo "⚠️ Chrome not found, installing Google Chrome..."
|
||||
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
|
||||
sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y google-chrome-stable
|
||||
fi
|
||||
|
||||
# Install project dependencies
|
||||
echo "📚 Installing project dependencies..."
|
||||
npm install -g pnpm
|
||||
pnpm install
|
||||
|
||||
# Build the project
|
||||
echo "🔨 Building the project..."
|
||||
pnpm run bundle
|
||||
|
||||
# Create a symlink for Chrome if needed
|
||||
if [ -f "/usr/bin/google-chrome-stable" ] && [ ! -f "/usr/bin/google-chrome" ]; then
|
||||
sudo ln -sf /usr/bin/google-chrome-stable /usr/bin/google-chrome
|
||||
fi
|
||||
|
||||
# Set up Docker if available
|
||||
if command -v docker &> /dev/null; then
|
||||
echo "🐳 Docker is available, pulling browserless/chrome image..."
|
||||
docker pull browserless/chrome:latest || true
|
||||
fi
|
||||
|
||||
echo "✅ Development environment setup complete!"
|
||||
echo ""
|
||||
echo "📝 Notes:"
|
||||
echo " - Chrome is installed at: $(which google-chrome || which google-chrome-stable || echo 'Not found')"
|
||||
echo " - Docker is available: $(command -v docker &> /dev/null && echo 'Yes' || echo 'No')"
|
||||
echo " - The browser tool should now work correctly in this Codespace"
|
||||
echo ""
|
||||
echo "🎉 Happy coding with Roo Code!"
|
||||
66
.devcontainer/post-start.sh
Normal file
66
.devcontainer/post-start.sh
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "🔄 Starting Roo Code development environment..."
|
||||
|
||||
# Check if Chrome is accessible
|
||||
if command -v google-chrome &> /dev/null || command -v google-chrome-stable &> /dev/null; then
|
||||
CHROME_PATH=$(which google-chrome || which google-chrome-stable)
|
||||
echo "✅ Chrome found at: $CHROME_PATH"
|
||||
|
||||
# Test Chrome can run headless
|
||||
echo "🧪 Testing Chrome headless mode..."
|
||||
timeout 5 $CHROME_PATH --headless --no-sandbox --disable-gpu --dump-dom https://example.com > /dev/null 2>&1 && \
|
||||
echo "✅ Chrome headless mode works!" || \
|
||||
echo "⚠️ Chrome headless test failed, but this might be okay"
|
||||
else
|
||||
echo "⚠️ Chrome not found. The browser tool may not work correctly."
|
||||
fi
|
||||
|
||||
# Check Docker availability
|
||||
if command -v docker &> /dev/null; then
|
||||
echo "🐳 Docker is available"
|
||||
|
||||
# Check if Docker daemon is running
|
||||
if docker info > /dev/null 2>&1; then
|
||||
echo "✅ Docker daemon is running"
|
||||
|
||||
# Optionally pre-pull the browserless image
|
||||
if [ "${PRELOAD_DOCKER_IMAGES:-false}" = "true" ]; then
|
||||
echo "📥 Pre-loading Docker browser image..."
|
||||
docker pull browserless/chrome:latest || true
|
||||
fi
|
||||
else
|
||||
echo "⚠️ Docker daemon is not running. Docker browser option won't be available."
|
||||
fi
|
||||
else
|
||||
echo "ℹ️ Docker is not available. Docker browser option won't be available."
|
||||
fi
|
||||
|
||||
# Display environment info
|
||||
echo ""
|
||||
echo "📊 Environment Information:"
|
||||
echo " - Node.js: $(node --version)"
|
||||
echo " - npm: $(npm --version)"
|
||||
echo " - pnpm: $(pnpm --version 2>/dev/null || echo 'Not installed')"
|
||||
echo " - Chrome: $(google-chrome --version 2>/dev/null || google-chrome-stable --version 2>/dev/null || echo 'Not installed')"
|
||||
echo " - Docker: $(docker --version 2>/dev/null || echo 'Not installed')"
|
||||
echo ""
|
||||
|
||||
# Check for missing dependencies
|
||||
MISSING_DEPS=()
|
||||
for lib in libatk-1.0.so.0 libatk-bridge-2.0.so.0 libcups.so.2; do
|
||||
if ! ldconfig -p | grep -q $lib; then
|
||||
MISSING_DEPS+=($lib)
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#MISSING_DEPS[@]} -gt 0 ]; then
|
||||
echo "⚠️ Some Chrome dependencies might be missing: ${MISSING_DEPS[*]}"
|
||||
echo " Run: sudo apt-get update && sudo apt-get install -y libatk1.0-0 libatk-bridge2.0-0 libcups2"
|
||||
else
|
||||
echo "✅ All critical Chrome dependencies are installed"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "🚀 Roo Code development environment is ready!"
|
||||
|
|
@ -407,6 +407,21 @@
|
|||
"minimum": 1,
|
||||
"maximum": 200,
|
||||
"description": "%settings.codeIndex.embeddingBatchSize.description%"
|
||||
},
|
||||
"roo-cline.browserDocker.enabled": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "%settings.browserDocker.enabled.description%"
|
||||
},
|
||||
"roo-cline.browserDocker.image": {
|
||||
"type": "string",
|
||||
"default": "browserless/chrome:latest",
|
||||
"description": "%settings.browserDocker.image.description%"
|
||||
},
|
||||
"roo-cline.browserDocker.autoStart": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "%settings.browserDocker.autoStart.description%"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,5 +41,8 @@
|
|||
"settings.useAgentRules.description": "Enable loading of AGENTS.md files for agent-specific rules (see https://agent-rules.org/)",
|
||||
"settings.apiRequestTimeout.description": "Maximum time in seconds to wait for API responses (0 = no timeout, 1-3600s, default: 600s). Higher values are recommended for local providers like LM Studio and Ollama that may need more processing time.",
|
||||
"settings.newTaskRequireTodos.description": "Require todos parameter when creating new tasks with the new_task tool",
|
||||
"settings.codeIndex.embeddingBatchSize.description": "The batch size for embedding operations during code indexing. Adjust this based on your API provider's limits. Default is 60."
|
||||
"settings.codeIndex.embeddingBatchSize.description": "The batch size for embedding operations during code indexing. Adjust this based on your API provider's limits. Default is 60.",
|
||||
"settings.browserDocker.enabled.description": "Enable Docker-based browser isolation for enhanced security and dependency management. Requires Docker to be installed.",
|
||||
"settings.browserDocker.image.description": "Docker image to use for the browser container (default: browserless/chrome:latest)",
|
||||
"settings.browserDocker.autoStart.description": "Automatically start the Docker browser container when needed"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,14 @@ import delay from "delay"
|
|||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { BrowserActionResult } from "../../shared/ExtensionMessage"
|
||||
import { discoverChromeHostUrl, tryChromeHostUrl } from "./browserDiscovery"
|
||||
import {
|
||||
detectEnvironment,
|
||||
installChromeDependencies,
|
||||
getSystemChromePath,
|
||||
getDockerBrowserConfig,
|
||||
startDockerBrowser,
|
||||
stopDockerBrowser,
|
||||
} from "./browserEnvironment"
|
||||
|
||||
// Timeout constants
|
||||
const BROWSER_NAVIGATION_TIMEOUT = 15_000 // 15 seconds
|
||||
|
|
@ -61,16 +69,77 @@ export class BrowserSession {
|
|||
}
|
||||
|
||||
/**
|
||||
* Launches a local browser instance
|
||||
* Launches a local browser instance with automatic dependency handling
|
||||
*/
|
||||
private async launchLocalBrowser(): Promise<void> {
|
||||
console.log("Launching local browser")
|
||||
|
||||
// Detect environment and check for missing dependencies
|
||||
const env = await detectEnvironment()
|
||||
|
||||
// In Codespaces or containers, try to install dependencies automatically
|
||||
if ((env.isCodespaces || env.isContainer) && env.missingDependencies.length > 0) {
|
||||
console.log("Detected missing Chrome dependencies, attempting automatic installation...")
|
||||
const installed = await installChromeDependencies(this.context)
|
||||
if (!installed) {
|
||||
// If automatic installation failed, try Docker as fallback
|
||||
const dockerConfig = getDockerBrowserConfig(this.context)
|
||||
if (dockerConfig.enabled && env.hasDocker) {
|
||||
console.log("Falling back to Docker browser...")
|
||||
const dockerEndpoint = await startDockerBrowser(dockerConfig)
|
||||
if (dockerEndpoint) {
|
||||
await this.connectWithChromeHostUrl(dockerEndpoint)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// If all else fails, show detailed error
|
||||
throw new Error(
|
||||
"Chrome dependencies are missing. Please install them manually:\n" +
|
||||
"sudo apt-get update && sudo apt-get install -y libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libasound2",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we should use Docker browser
|
||||
const dockerConfig = getDockerBrowserConfig(this.context)
|
||||
if (dockerConfig.enabled && dockerConfig.autoStart && env.hasDocker) {
|
||||
console.log("Using Docker browser as configured...")
|
||||
const dockerEndpoint = await startDockerBrowser(dockerConfig)
|
||||
if (dockerEndpoint) {
|
||||
await this.connectWithChromeHostUrl(dockerEndpoint)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Try to use system Chrome if available
|
||||
const systemChromePath = await getSystemChromePath()
|
||||
let executablePath: string
|
||||
|
||||
if (systemChromePath) {
|
||||
console.log(`Using system Chrome at: ${systemChromePath}`)
|
||||
executablePath = systemChromePath
|
||||
} else {
|
||||
// Fall back to puppeteer-chromium-resolver
|
||||
const stats = await this.ensureChromiumExists()
|
||||
executablePath = stats.executablePath
|
||||
}
|
||||
|
||||
// Prepare launch arguments
|
||||
const args = [
|
||||
"--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
|
||||
]
|
||||
|
||||
// Add sandbox flags for Linux/container environments
|
||||
if (env.isLinux || env.isContainer || env.isCodespaces) {
|
||||
args.push("--no-sandbox", "--disable-setuid-sandbox")
|
||||
}
|
||||
|
||||
// Launch the browser
|
||||
const stats = await this.ensureChromiumExists()
|
||||
this.browser = await stats.puppeteer.launch({
|
||||
args: [
|
||||
"--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
|
||||
],
|
||||
executablePath: stats.executablePath,
|
||||
args,
|
||||
executablePath,
|
||||
defaultViewport: this.getViewport(),
|
||||
// headless: false,
|
||||
})
|
||||
|
|
@ -205,6 +274,13 @@ export class BrowserSession {
|
|||
}
|
||||
this.resetBrowserState()
|
||||
}
|
||||
|
||||
// Stop Docker browser if it was started
|
||||
const dockerConfig = getDockerBrowserConfig(this.context)
|
||||
if (dockerConfig.enabled) {
|
||||
await stopDockerBrowser().catch(() => {})
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import TurndownService from "turndown"
|
|||
import PCR from "puppeteer-chromium-resolver"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { serializeError } from "serialize-error"
|
||||
import { detectEnvironment, installChromeDependencies, getSystemChromePath } from "./browserEnvironment"
|
||||
|
||||
// Timeout constants
|
||||
const URL_FETCH_TIMEOUT = 30_000 // 30 seconds
|
||||
|
|
@ -49,7 +50,35 @@ export class UrlContentFetcher {
|
|||
if (this.browser) {
|
||||
return
|
||||
}
|
||||
const stats = await this.ensureChromiumExists()
|
||||
|
||||
// Detect environment and check for missing dependencies
|
||||
const env = await detectEnvironment()
|
||||
|
||||
// In Codespaces or containers, try to install dependencies automatically
|
||||
if ((env.isCodespaces || env.isContainer) && env.missingDependencies.length > 0) {
|
||||
console.log("Detected missing Chrome dependencies, attempting automatic installation...")
|
||||
const installed = await installChromeDependencies(this.context)
|
||||
if (!installed) {
|
||||
throw new Error(
|
||||
"Chrome dependencies are missing. Please install them manually:\n" +
|
||||
"sudo apt-get update && sudo apt-get install -y libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libasound2",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Try to use system Chrome if available
|
||||
const systemChromePath = await getSystemChromePath()
|
||||
let executablePath: string
|
||||
|
||||
if (systemChromePath) {
|
||||
console.log(`Using system Chrome at: ${systemChromePath}`)
|
||||
executablePath = systemChromePath
|
||||
} else {
|
||||
// Fall back to puppeteer-chromium-resolver
|
||||
const stats = await this.ensureChromiumExists()
|
||||
executablePath = stats.executablePath
|
||||
}
|
||||
|
||||
const args = [
|
||||
"--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
|
||||
"--disable-dev-shm-usage",
|
||||
|
|
@ -58,13 +87,16 @@ export class UrlContentFetcher {
|
|||
"--disable-gpu",
|
||||
"--disable-features=VizDisplayCompositor",
|
||||
]
|
||||
if (process.platform === "linux") {
|
||||
// Fixes network errors on Linux hosts (see https://github.com/puppeteer/puppeteer/issues/8246)
|
||||
args.push("--no-sandbox")
|
||||
|
||||
// Add sandbox flags for Linux/container environments
|
||||
if (env.isLinux || env.isContainer || env.isCodespaces) {
|
||||
args.push("--no-sandbox", "--disable-setuid-sandbox")
|
||||
}
|
||||
|
||||
const stats = await this.ensureChromiumExists()
|
||||
this.browser = await stats.puppeteer.launch({
|
||||
args,
|
||||
executablePath: stats.executablePath,
|
||||
executablePath,
|
||||
})
|
||||
// (latest version of puppeteer does not add headless to user agent)
|
||||
this.page = await this.browser?.newPage()
|
||||
|
|
|
|||
|
|
@ -9,6 +9,19 @@ vi.mock("vscode", () => ({
|
|||
Uri: {
|
||||
file: vi.fn((path) => ({ fsPath: path })),
|
||||
},
|
||||
window: {
|
||||
showErrorMessage: vi.fn(),
|
||||
showInformationMessage: vi.fn(),
|
||||
withProgress: vi.fn((options, task) => task({ report: vi.fn() })),
|
||||
},
|
||||
workspace: {
|
||||
getConfiguration: vi.fn(() => ({
|
||||
get: vi.fn((key, defaultValue) => defaultValue),
|
||||
})),
|
||||
},
|
||||
ProgressLocation: {
|
||||
Notification: 15,
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock puppeteer-core
|
||||
|
|
@ -69,6 +82,27 @@ vi.mock("../browserDiscovery", () => ({
|
|||
tryChromeHostUrl: vi.fn().mockResolvedValue(false),
|
||||
}))
|
||||
|
||||
// Mock browserEnvironment
|
||||
vi.mock("../browserEnvironment", () => ({
|
||||
detectEnvironment: vi.fn().mockResolvedValue({
|
||||
isCodespaces: false,
|
||||
isContainer: false,
|
||||
isLinux: false,
|
||||
hasDocker: false,
|
||||
hasSystemChrome: false,
|
||||
missingDependencies: [],
|
||||
}),
|
||||
installChromeDependencies: vi.fn().mockResolvedValue(true),
|
||||
getSystemChromePath: vi.fn().mockResolvedValue(null),
|
||||
getDockerBrowserConfig: vi.fn().mockReturnValue({
|
||||
enabled: false,
|
||||
image: "browserless/chrome:latest",
|
||||
autoStart: true,
|
||||
}),
|
||||
startDockerBrowser: vi.fn().mockResolvedValue(null),
|
||||
stopDockerBrowser: vi.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
|
||||
// Mock delay
|
||||
vi.mock("delay", () => ({
|
||||
default: vi.fn().mockResolvedValue(undefined),
|
||||
|
|
@ -83,9 +117,29 @@ describe("BrowserSession", () => {
|
|||
let browserSession: BrowserSession
|
||||
let mockContext: any
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Import and clear mocks for browserEnvironment
|
||||
const browserEnv = await import("../browserEnvironment")
|
||||
vi.mocked(browserEnv.detectEnvironment).mockResolvedValue({
|
||||
isCodespaces: false,
|
||||
isContainer: false,
|
||||
isLinux: false,
|
||||
hasDocker: false,
|
||||
hasSystemChrome: false,
|
||||
missingDependencies: [],
|
||||
})
|
||||
vi.mocked(browserEnv.installChromeDependencies).mockResolvedValue(true)
|
||||
vi.mocked(browserEnv.getSystemChromePath).mockResolvedValue(null)
|
||||
vi.mocked(browserEnv.getDockerBrowserConfig).mockReturnValue({
|
||||
enabled: false,
|
||||
image: "browserless/chrome:latest",
|
||||
autoStart: true,
|
||||
})
|
||||
vi.mocked(browserEnv.startDockerBrowser).mockResolvedValue(null)
|
||||
vi.mocked(browserEnv.stopDockerBrowser).mockResolvedValue(undefined)
|
||||
|
||||
// Set up mock context
|
||||
mockContext = {
|
||||
globalState: {
|
||||
|
|
|
|||
391
src/services/browser/__tests__/browserEnvironment.spec.ts
Normal file
391
src/services/browser/__tests__/browserEnvironment.spec.ts
Normal file
|
|
@ -0,0 +1,391 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
|
||||
|
||||
// Use vi.hoisted to ensure mocks are set up before imports
|
||||
const { execAsync } = vi.hoisted(() => {
|
||||
return {
|
||||
execAsync: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
// Mock modules before any imports
|
||||
vi.mock("fs/promises")
|
||||
vi.mock("child_process")
|
||||
vi.mock("util", () => ({
|
||||
promisify: vi.fn(() => execAsync),
|
||||
}))
|
||||
vi.mock("vscode", () => ({
|
||||
window: {
|
||||
showErrorMessage: vi.fn(),
|
||||
showInformationMessage: vi.fn(),
|
||||
withProgress: vi.fn((options, task) => task({ report: vi.fn() })),
|
||||
},
|
||||
workspace: {
|
||||
getConfiguration: vi.fn(() => ({
|
||||
get: vi.fn((key, defaultValue) => defaultValue),
|
||||
})),
|
||||
},
|
||||
ProgressLocation: {
|
||||
Notification: 15,
|
||||
},
|
||||
}))
|
||||
|
||||
// Now import the modules after mocking
|
||||
import * as fs from "fs/promises"
|
||||
import * as vscode from "vscode"
|
||||
import {
|
||||
detectEnvironment,
|
||||
installChromeDependencies,
|
||||
getSystemChromePath,
|
||||
getDockerBrowserConfig,
|
||||
startDockerBrowser,
|
||||
stopDockerBrowser,
|
||||
} from "../browserEnvironment"
|
||||
|
||||
describe("browserEnvironment", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Reset environment variables
|
||||
delete process.env.CODESPACES
|
||||
// Mock platform
|
||||
Object.defineProperty(process, "platform", {
|
||||
value: "linux",
|
||||
writable: true,
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe("detectEnvironment", () => {
|
||||
it("should detect Codespaces environment", async () => {
|
||||
process.env.CODESPACES = "true"
|
||||
vi.mocked(fs.access).mockRejectedValue(new Error("No .dockerenv"))
|
||||
vi.mocked(fs.readFile).mockRejectedValue(new Error("No cgroup"))
|
||||
execAsync.mockRejectedValue(new Error("No docker"))
|
||||
|
||||
const env = await detectEnvironment()
|
||||
|
||||
expect(env.isCodespaces).toBe(true)
|
||||
expect(env.isLinux).toBe(true)
|
||||
})
|
||||
|
||||
it("should detect container environment via .dockerenv", async () => {
|
||||
vi.mocked(fs.access).mockResolvedValue(undefined)
|
||||
execAsync.mockRejectedValue(new Error("No docker"))
|
||||
|
||||
const env = await detectEnvironment()
|
||||
|
||||
expect(env.isContainer).toBe(true)
|
||||
})
|
||||
|
||||
it("should detect container environment via cgroup", async () => {
|
||||
vi.mocked(fs.access).mockRejectedValue(new Error("No .dockerenv"))
|
||||
vi.mocked(fs.readFile).mockResolvedValue("1:name=systemd:/docker/abc123")
|
||||
execAsync.mockRejectedValue(new Error("No docker"))
|
||||
|
||||
const env = await detectEnvironment()
|
||||
|
||||
expect(env.isContainer).toBe(true)
|
||||
})
|
||||
|
||||
it("should detect Docker availability", async () => {
|
||||
vi.mocked(fs.access).mockRejectedValue(new Error("No .dockerenv"))
|
||||
vi.mocked(fs.readFile).mockRejectedValue(new Error("No cgroup"))
|
||||
execAsync.mockImplementation((cmd: string) => {
|
||||
if (cmd === "docker --version") {
|
||||
return Promise.resolve({ stdout: "Docker version 20.10.0", stderr: "" })
|
||||
}
|
||||
if (cmd.includes("ldconfig")) {
|
||||
return Promise.resolve({ stdout: "", stderr: "" })
|
||||
}
|
||||
return Promise.reject(new Error("Command not found"))
|
||||
})
|
||||
|
||||
const env = await detectEnvironment()
|
||||
|
||||
expect(env.hasDocker).toBe(true)
|
||||
})
|
||||
|
||||
it("should detect system Chrome", async () => {
|
||||
vi.mocked(fs.access).mockImplementation((path) => {
|
||||
if (path === "/usr/bin/google-chrome") {
|
||||
return Promise.resolve(undefined)
|
||||
}
|
||||
return Promise.reject(new Error("Not found"))
|
||||
})
|
||||
execAsync.mockRejectedValue(new Error("No docker"))
|
||||
|
||||
const env = await detectEnvironment()
|
||||
|
||||
expect(env.hasSystemChrome).toBe(true)
|
||||
})
|
||||
|
||||
it("should detect missing dependencies on Linux", async () => {
|
||||
vi.mocked(fs.access).mockRejectedValue(new Error("No .dockerenv"))
|
||||
vi.mocked(fs.readFile).mockRejectedValue(new Error("No cgroup"))
|
||||
execAsync.mockImplementation((cmd: string) => {
|
||||
if (cmd.includes("ldconfig")) {
|
||||
// Simulate missing libatk-1.0.so.0
|
||||
if (cmd.includes("libatk-1.0.so.0")) {
|
||||
return Promise.reject(new Error("Not found"))
|
||||
}
|
||||
return Promise.resolve({ stdout: "library found", stderr: "" })
|
||||
}
|
||||
return Promise.reject(new Error("Command not found"))
|
||||
})
|
||||
|
||||
const env = await detectEnvironment()
|
||||
|
||||
expect(env.missingDependencies).toContain("libatk-1.0.so.0")
|
||||
})
|
||||
|
||||
it("should not check dependencies on non-Linux platforms", async () => {
|
||||
Object.defineProperty(process, "platform", {
|
||||
value: "darwin",
|
||||
writable: true,
|
||||
})
|
||||
vi.mocked(fs.access).mockRejectedValue(new Error("No .dockerenv"))
|
||||
vi.mocked(fs.readFile).mockRejectedValue(new Error("No cgroup"))
|
||||
execAsync.mockRejectedValue(new Error("No docker"))
|
||||
|
||||
const env = await detectEnvironment()
|
||||
|
||||
expect(env.isLinux).toBe(false)
|
||||
expect(env.missingDependencies).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe("getSystemChromePath", () => {
|
||||
it("should return path to Google Chrome if found", async () => {
|
||||
vi.mocked(fs.access).mockImplementation((path) => {
|
||||
if (path === "/usr/bin/google-chrome") {
|
||||
return Promise.resolve(undefined)
|
||||
}
|
||||
return Promise.reject(new Error("Not found"))
|
||||
})
|
||||
|
||||
const path = await getSystemChromePath()
|
||||
|
||||
expect(path).toBe("/usr/bin/google-chrome")
|
||||
})
|
||||
|
||||
it("should return path to Chromium if Chrome not found", async () => {
|
||||
vi.mocked(fs.access).mockImplementation((path) => {
|
||||
if (path === "/usr/bin/chromium") {
|
||||
return Promise.resolve(undefined)
|
||||
}
|
||||
return Promise.reject(new Error("Not found"))
|
||||
})
|
||||
|
||||
const path = await getSystemChromePath()
|
||||
|
||||
expect(path).toBe("/usr/bin/chromium")
|
||||
})
|
||||
|
||||
it("should return null if no Chrome/Chromium found", async () => {
|
||||
vi.mocked(fs.access).mockRejectedValue(new Error("Not found"))
|
||||
|
||||
const path = await getSystemChromePath()
|
||||
|
||||
expect(path).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("installChromeDependencies", () => {
|
||||
it("should skip installation on non-Linux", async () => {
|
||||
Object.defineProperty(process, "platform", {
|
||||
value: "darwin",
|
||||
writable: true,
|
||||
})
|
||||
// Mock execAsync for detectEnvironment calls
|
||||
execAsync.mockRejectedValue(new Error("Command not found"))
|
||||
|
||||
const mockContext = {} as vscode.ExtensionContext
|
||||
|
||||
const result = await installChromeDependencies(mockContext)
|
||||
|
||||
expect(result).toBe(true)
|
||||
// execAsync may be called for environment detection, but not for installation
|
||||
expect(execAsync).not.toHaveBeenCalledWith(expect.stringContaining("apt-get"))
|
||||
expect(execAsync).not.toHaveBeenCalledWith(expect.stringContaining("sudo"))
|
||||
})
|
||||
|
||||
it("should skip installation if no dependencies missing", async () => {
|
||||
vi.mocked(fs.access).mockRejectedValue(new Error("No .dockerenv"))
|
||||
vi.mocked(fs.readFile).mockRejectedValue(new Error("No cgroup"))
|
||||
execAsync.mockImplementation((cmd: string) => {
|
||||
if (cmd.includes("ldconfig")) {
|
||||
return Promise.resolve({ stdout: "library found", stderr: "" })
|
||||
}
|
||||
return Promise.reject(new Error("Command not found"))
|
||||
})
|
||||
const mockContext = {} as vscode.ExtensionContext
|
||||
|
||||
const result = await installChromeDependencies(mockContext)
|
||||
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it("should show error if no sudo access", async () => {
|
||||
vi.mocked(fs.access).mockRejectedValue(new Error("No .dockerenv"))
|
||||
vi.mocked(fs.readFile).mockRejectedValue(new Error("No cgroup"))
|
||||
execAsync.mockImplementation((cmd: string) => {
|
||||
if (cmd === "sudo -n true") {
|
||||
return Promise.reject(new Error("No sudo"))
|
||||
}
|
||||
if (cmd.includes("ldconfig")) {
|
||||
if (cmd.includes("libatk-1.0.so.0")) {
|
||||
return Promise.reject(new Error("Not found"))
|
||||
}
|
||||
return Promise.resolve({ stdout: "library found", stderr: "" })
|
||||
}
|
||||
return Promise.reject(new Error("Command not found"))
|
||||
})
|
||||
const mockContext = {} as vscode.ExtensionContext
|
||||
|
||||
const result = await installChromeDependencies(mockContext)
|
||||
|
||||
expect(result).toBe(false)
|
||||
expect(vscode.window.showErrorMessage).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should install dependencies with sudo access", async () => {
|
||||
vi.mocked(fs.access).mockRejectedValue(new Error("No .dockerenv"))
|
||||
vi.mocked(fs.readFile).mockRejectedValue(new Error("No cgroup"))
|
||||
execAsync.mockImplementation((cmd: string) => {
|
||||
if (cmd === "sudo -n true") {
|
||||
return Promise.resolve({ stdout: "", stderr: "" })
|
||||
}
|
||||
if (cmd.includes("ldconfig")) {
|
||||
if (cmd.includes("libatk-1.0.so.0")) {
|
||||
return Promise.reject(new Error("Not found"))
|
||||
}
|
||||
return Promise.resolve({ stdout: "library found", stderr: "" })
|
||||
}
|
||||
if (cmd.includes("apt-get")) {
|
||||
return Promise.resolve({ stdout: "", stderr: "" })
|
||||
}
|
||||
if (cmd.includes("which google-chrome")) {
|
||||
return Promise.reject(new Error("Not found"))
|
||||
}
|
||||
return Promise.resolve({ stdout: "", stderr: "" })
|
||||
})
|
||||
const mockContext = {} as vscode.ExtensionContext
|
||||
|
||||
const result = await installChromeDependencies(mockContext)
|
||||
|
||||
expect(result).toBe(true)
|
||||
expect(execAsync).toHaveBeenCalledWith("sudo apt-get update")
|
||||
expect(execAsync).toHaveBeenCalledWith(expect.stringContaining("sudo apt-get install -y"))
|
||||
expect(vscode.window.showInformationMessage).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("getDockerBrowserConfig", () => {
|
||||
it("should return default configuration", () => {
|
||||
const mockContext = {} as vscode.ExtensionContext
|
||||
|
||||
const config = getDockerBrowserConfig(mockContext)
|
||||
|
||||
expect(config).toEqual({
|
||||
enabled: false,
|
||||
image: "browserless/chrome:latest",
|
||||
autoStart: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("should return custom configuration from settings", () => {
|
||||
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
|
||||
get: vi.fn((key, defaultValue) => {
|
||||
if (key === "browserDocker.enabled") return true
|
||||
if (key === "browserDocker.image") return "custom/chrome:v1"
|
||||
if (key === "browserDocker.autoStart") return false
|
||||
return defaultValue
|
||||
}),
|
||||
} as any)
|
||||
const mockContext = {} as vscode.ExtensionContext
|
||||
|
||||
const config = getDockerBrowserConfig(mockContext)
|
||||
|
||||
expect(config).toEqual({
|
||||
enabled: true,
|
||||
image: "custom/chrome:v1",
|
||||
autoStart: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("startDockerBrowser", () => {
|
||||
it("should start new Docker container", async () => {
|
||||
execAsync.mockImplementation((cmd: string) => {
|
||||
if (cmd.includes("docker ps -a")) {
|
||||
return Promise.resolve({ stdout: "", stderr: "" })
|
||||
}
|
||||
if (cmd.includes("docker run")) {
|
||||
return Promise.resolve({ stdout: "", stderr: "" })
|
||||
}
|
||||
return Promise.resolve({ stdout: "", stderr: "" })
|
||||
})
|
||||
|
||||
const endpoint = await startDockerBrowser({
|
||||
enabled: true,
|
||||
image: "browserless/chrome:latest",
|
||||
autoStart: true,
|
||||
})
|
||||
|
||||
expect(endpoint).toBe("ws://localhost:3000")
|
||||
expect(execAsync).toHaveBeenCalledWith(expect.stringContaining("docker run"))
|
||||
})
|
||||
|
||||
it("should start existing Docker container", async () => {
|
||||
execAsync.mockImplementation((cmd: string) => {
|
||||
if (cmd.includes("docker ps -a")) {
|
||||
return Promise.resolve({ stdout: "roo-browser", stderr: "" })
|
||||
}
|
||||
if (cmd.includes("docker start")) {
|
||||
return Promise.resolve({ stdout: "", stderr: "" })
|
||||
}
|
||||
return Promise.resolve({ stdout: "", stderr: "" })
|
||||
})
|
||||
|
||||
const endpoint = await startDockerBrowser({
|
||||
enabled: true,
|
||||
image: "browserless/chrome:latest",
|
||||
autoStart: true,
|
||||
})
|
||||
|
||||
expect(endpoint).toBe("ws://localhost:3000")
|
||||
expect(execAsync).toHaveBeenCalledWith("docker start roo-browser")
|
||||
expect(execAsync).not.toHaveBeenCalledWith(expect.stringContaining("docker run"))
|
||||
})
|
||||
|
||||
it("should return null on error", async () => {
|
||||
execAsync.mockRejectedValue(new Error("Docker error"))
|
||||
|
||||
const endpoint = await startDockerBrowser({
|
||||
enabled: true,
|
||||
image: "browserless/chrome:latest",
|
||||
autoStart: true,
|
||||
})
|
||||
|
||||
expect(endpoint).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe("stopDockerBrowser", () => {
|
||||
it("should stop Docker container", async () => {
|
||||
execAsync.mockResolvedValue({ stdout: "", stderr: "" })
|
||||
|
||||
await stopDockerBrowser()
|
||||
|
||||
expect(execAsync).toHaveBeenCalledWith("docker stop roo-browser")
|
||||
})
|
||||
|
||||
it("should not throw on error", async () => {
|
||||
execAsync.mockRejectedValue(new Error("Container not running"))
|
||||
|
||||
await expect(stopDockerBrowser()).resolves.not.toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
334
src/services/browser/browserEnvironment.ts
Normal file
334
src/services/browser/browserEnvironment.ts
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
import * as vscode from "vscode"
|
||||
import { exec } from "child_process"
|
||||
import { promisify } from "util"
|
||||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
|
||||
const execAsync = promisify(exec)
|
||||
|
||||
/**
|
||||
* Detects the current environment and its characteristics
|
||||
*/
|
||||
export interface EnvironmentInfo {
|
||||
isCodespaces: boolean
|
||||
isContainer: boolean
|
||||
isLinux: boolean
|
||||
hasDocker: boolean
|
||||
hasSystemChrome: boolean
|
||||
missingDependencies: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Chrome/Chromium dependencies required for headless operation
|
||||
*/
|
||||
const CHROME_DEPENDENCIES = [
|
||||
"libatk-1.0.so.0",
|
||||
"libatk-bridge-2.0.so.0",
|
||||
"libcups.so.2",
|
||||
"libdrm.so.2",
|
||||
"libxkbcommon.so.0",
|
||||
"libxcomposite.so.1",
|
||||
"libxdamage.so.1",
|
||||
"libxfixes.so.3",
|
||||
"libxrandr.so.2",
|
||||
"libgbm.so.1",
|
||||
"libasound.so.2",
|
||||
"libatspi.so.0",
|
||||
"libgtk-3.so.0",
|
||||
"libpango-1.0.so.0",
|
||||
"libcairo.so.2",
|
||||
"libxshmfence.so.1",
|
||||
"libnss3.so",
|
||||
"libnssutil3.so",
|
||||
"libnspr4.so",
|
||||
]
|
||||
|
||||
/**
|
||||
* Package names for installing Chrome dependencies
|
||||
*/
|
||||
const DEPENDENCY_PACKAGES = [
|
||||
"libatk1.0-0",
|
||||
"libatk-bridge2.0-0",
|
||||
"libcups2",
|
||||
"libdrm2",
|
||||
"libxkbcommon0",
|
||||
"libxcomposite1",
|
||||
"libxdamage1",
|
||||
"libxfixes3",
|
||||
"libxrandr2",
|
||||
"libgbm1",
|
||||
"libasound2",
|
||||
"libatspi2.0-0",
|
||||
"libgtk-3-0",
|
||||
"libpango-1.0-0",
|
||||
"libcairo2",
|
||||
"libxshmfence1",
|
||||
"libnss3",
|
||||
"libnssutil3",
|
||||
"libnspr4",
|
||||
"libx11-xcb1",
|
||||
"libxcb-dri3-0",
|
||||
]
|
||||
|
||||
/**
|
||||
* Detects the current environment
|
||||
*/
|
||||
export async function detectEnvironment(): Promise<EnvironmentInfo> {
|
||||
const isCodespaces = process.env.CODESPACES === "true"
|
||||
const isContainer = await checkIfContainer()
|
||||
const isLinux = process.platform === "linux"
|
||||
const hasDocker = await checkDockerAvailable()
|
||||
const hasSystemChrome = await checkSystemChrome()
|
||||
const missingDependencies = isLinux ? await checkMissingDependencies() : []
|
||||
|
||||
return {
|
||||
isCodespaces,
|
||||
isContainer,
|
||||
isLinux,
|
||||
hasDocker,
|
||||
hasSystemChrome,
|
||||
missingDependencies,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if running inside a container
|
||||
*/
|
||||
async function checkIfContainer(): Promise<boolean> {
|
||||
try {
|
||||
// Check for .dockerenv file
|
||||
await fs.access("/.dockerenv")
|
||||
return true
|
||||
} catch {
|
||||
// Check for container indicators in cgroup
|
||||
try {
|
||||
const cgroup = await fs.readFile("/proc/1/cgroup", "utf-8")
|
||||
return cgroup.includes("docker") || cgroup.includes("containerd") || cgroup.includes("kubepods")
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if Docker is available
|
||||
*/
|
||||
async function checkDockerAvailable(): Promise<boolean> {
|
||||
try {
|
||||
const { stdout } = await execAsync("docker --version")
|
||||
return stdout.includes("Docker")
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if system Chrome/Chromium is available
|
||||
*/
|
||||
async function checkSystemChrome(): Promise<boolean> {
|
||||
const chromePaths = [
|
||||
"/usr/bin/google-chrome",
|
||||
"/usr/bin/google-chrome-stable",
|
||||
"/usr/bin/chromium",
|
||||
"/usr/bin/chromium-browser",
|
||||
"/snap/bin/chromium",
|
||||
]
|
||||
|
||||
for (const chromePath of chromePaths) {
|
||||
try {
|
||||
await fs.access(chromePath)
|
||||
return true
|
||||
} catch {
|
||||
// Continue checking other paths
|
||||
}
|
||||
}
|
||||
|
||||
// Also check via which command
|
||||
try {
|
||||
await execAsync("which google-chrome || which chromium || which chromium-browser")
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for missing Chrome dependencies
|
||||
*/
|
||||
async function checkMissingDependencies(): Promise<string[]> {
|
||||
const missing: string[] = []
|
||||
|
||||
for (const dep of CHROME_DEPENDENCIES) {
|
||||
try {
|
||||
// Try to find the library using ldconfig
|
||||
const { stdout } = await execAsync(`ldconfig -p | grep ${dep}`)
|
||||
if (!stdout) {
|
||||
missing.push(dep)
|
||||
}
|
||||
} catch {
|
||||
missing.push(dep)
|
||||
}
|
||||
}
|
||||
|
||||
return missing
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to install missing Chrome dependencies
|
||||
*/
|
||||
export async function installChromeDependencies(context: vscode.ExtensionContext): Promise<boolean> {
|
||||
const env = await detectEnvironment()
|
||||
|
||||
if (!env.isLinux || env.missingDependencies.length === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if we have sudo access
|
||||
try {
|
||||
await execAsync("sudo -n true")
|
||||
} catch {
|
||||
vscode.window.showErrorMessage(
|
||||
"Chrome dependencies are missing but sudo access is required to install them. " +
|
||||
"Please run: sudo apt-get update && sudo apt-get install -y " +
|
||||
DEPENDENCY_PACKAGES.join(" "),
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
// Show progress notification
|
||||
return vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: "Installing Chrome dependencies...",
|
||||
cancellable: false,
|
||||
},
|
||||
async (progress) => {
|
||||
try {
|
||||
progress.report({ message: "Updating package lists..." })
|
||||
await execAsync("sudo apt-get update")
|
||||
|
||||
progress.report({ message: "Installing dependencies..." })
|
||||
const packages = DEPENDENCY_PACKAGES.join(" ")
|
||||
await execAsync(`sudo apt-get install -y ${packages}`)
|
||||
|
||||
// Install Chrome if not present
|
||||
if (!env.hasSystemChrome) {
|
||||
progress.report({ message: "Installing Google Chrome..." })
|
||||
await installChrome()
|
||||
}
|
||||
|
||||
vscode.window.showInformationMessage("Chrome dependencies installed successfully!")
|
||||
return true
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to install Chrome dependencies: ${error.message}. ` +
|
||||
"Please install them manually with: sudo apt-get install -y " +
|
||||
DEPENDENCY_PACKAGES.join(" "),
|
||||
)
|
||||
return false
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs Google Chrome
|
||||
*/
|
||||
async function installChrome(): Promise<void> {
|
||||
const commands = [
|
||||
"wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -",
|
||||
"sudo sh -c 'echo \"deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main\" >> /etc/apt/sources.list.d/google.list'",
|
||||
"sudo apt-get update",
|
||||
"sudo apt-get install -y google-chrome-stable",
|
||||
]
|
||||
|
||||
for (const cmd of commands) {
|
||||
await execAsync(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the path to system Chrome/Chromium if available
|
||||
*/
|
||||
export async function getSystemChromePath(): Promise<string | null> {
|
||||
const chromePaths = [
|
||||
"/usr/bin/google-chrome",
|
||||
"/usr/bin/google-chrome-stable",
|
||||
"/usr/bin/chromium",
|
||||
"/usr/bin/chromium-browser",
|
||||
"/snap/bin/chromium",
|
||||
]
|
||||
|
||||
for (const chromePath of chromePaths) {
|
||||
try {
|
||||
await fs.access(chromePath)
|
||||
return chromePath
|
||||
} catch {
|
||||
// Continue checking other paths
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for Docker-based browser
|
||||
*/
|
||||
export interface DockerBrowserConfig {
|
||||
enabled: boolean
|
||||
image: string
|
||||
autoStart: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets Docker browser configuration from settings
|
||||
*/
|
||||
export function getDockerBrowserConfig(context: vscode.ExtensionContext): DockerBrowserConfig {
|
||||
const config = vscode.workspace.getConfiguration("roo-code")
|
||||
|
||||
return {
|
||||
enabled: config.get<boolean>("browserDocker.enabled", false),
|
||||
image: config.get<string>("browserDocker.image", "browserless/chrome:latest"),
|
||||
autoStart: config.get<boolean>("browserDocker.autoStart", true),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a Docker container for browser operations
|
||||
*/
|
||||
export async function startDockerBrowser(config: DockerBrowserConfig): Promise<string | null> {
|
||||
try {
|
||||
// Check if container already exists
|
||||
const { stdout: existingContainer } = await execAsync(
|
||||
"docker ps -a --filter name=roo-browser --format '{{.Names}}'",
|
||||
)
|
||||
|
||||
if (existingContainer.includes("roo-browser")) {
|
||||
// Start existing container
|
||||
await execAsync("docker start roo-browser")
|
||||
} else {
|
||||
// Create and start new container
|
||||
await execAsync(`docker run -d --name roo-browser -p 3000:3000 --rm ${config.image}`)
|
||||
}
|
||||
|
||||
// Wait for container to be ready
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000))
|
||||
|
||||
// Return the WebSocket endpoint
|
||||
return "ws://localhost:3000"
|
||||
} catch (error) {
|
||||
console.error("Failed to start Docker browser:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the Docker browser container
|
||||
*/
|
||||
export async function stopDockerBrowser(): Promise<void> {
|
||||
try {
|
||||
await execAsync("docker stop roo-browser")
|
||||
} catch {
|
||||
// Container might not be running
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue