diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..fa31df02d6 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,36 @@ +{ + "name": "Roo Code Development", + "image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye", + + // Features to add to the dev container + "features": { + "ghcr.io/devcontainers/features/github-cli:1": {} + }, + + // Install Chromium and its dependencies automatically + "postCreateCommand": "sudo apt-get update && sudo apt --fix-broken install -y && sudo apt-get install -y chromium-browser chromium-codecs-ffmpeg chromium-codecs-ffmpeg-extra libatk-bridge2.0-0 libatk1.0-0 libatspi2.0-0 libcups2 libdbus-1-3 libdrm2 libgbm1 libgtk-3-0 libnspr4 libnss3 libx11-xcb1 libxcomposite1 libxdamage1 libxfixes3 libxkbcommon0 libxrandr2 xdg-utils && npm install", + + // Configure VS Code settings + "customizations": { + "vscode": { + "extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "ms-vscode.vscode-typescript-next"], + "settings": { + "terminal.integrated.defaultProfile.linux": "bash", + "typescript.tsdk": "node_modules/typescript/lib" + } + } + }, + + // Forward ports for development servers + "forwardPorts": [3000, 5173, 8080], + + // Environment variables + "remoteEnv": { + "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD": "true", + "PUPPETEER_EXECUTABLE_PATH": "/usr/bin/chromium-browser", + "CODESPACES": "true" + }, + + // Run as non-root user + "remoteUser": "node" +} diff --git a/src/services/browser/BrowserSession.ts b/src/services/browser/BrowserSession.ts index 75b432f01d..34c6c715b8 100644 --- a/src/services/browser/BrowserSession.ts +++ b/src/services/browser/BrowserSession.ts @@ -9,6 +9,7 @@ import delay from "delay" import { fileExistsAtPath } from "../../utils/fs" import { BrowserActionResult } from "../../shared/ExtensionMessage" import { discoverChromeHostUrl, tryChromeHostUrl } from "./browserDiscovery" +import { isCodespacesEnvironment, fixCodespaceDependencies, isMissingDependencyError } from "./codespaceUtils" // Timeout constants const BROWSER_NAVIGATION_TIMEOUT = 15_000 // 15 seconds @@ -42,13 +43,35 @@ export class BrowserSession { await fs.mkdir(puppeteerDir, { recursive: true }) } - // if chromium doesn't exist, this will download it to path.join(puppeteerDir, ".chromium-browser-snapshots") - // if it does exist it will return the path to existing chromium - const stats: PCRStats = await PCR({ - downloadPath: puppeteerDir, - }) + try { + // if chromium doesn't exist, this will download it to path.join(puppeteerDir, ".chromium-browser-snapshots") + // if it does exist it will return the path to existing chromium + const stats: PCRStats = await PCR({ + downloadPath: puppeteerDir, + }) - return stats + return stats + } catch (error) { + // Check if this is a missing dependency error in Codespaces + if (isCodespacesEnvironment() && isMissingDependencyError(error)) { + console.log("Detected missing browser dependencies in Codespaces, attempting to fix...") + + // Try to fix the dependencies + const fixed = await fixCodespaceDependencies() + + if (fixed) { + // Retry PCR after fixing dependencies + console.log("Dependencies fixed, retrying browser initialization...") + const stats: PCRStats = await PCR({ + downloadPath: puppeteerDir, + }) + return stats + } + } + + // If we couldn't fix it or it's not a Codespaces issue, throw the original error + throw error + } } /** @@ -65,16 +88,60 @@ export class BrowserSession { */ private async launchLocalBrowser(): Promise { console.log("Launching local browser") - const stats = await this.ensureChromiumExists() - this.browser = await stats.puppeteer.launch({ - args: [ + + try { + const stats = await this.ensureChromiumExists() + + 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", - ], - executablePath: stats.executablePath, - defaultViewport: this.getViewport(), - // headless: false, - }) - this.isUsingRemoteBrowser = false + ] + + // Add additional args for Linux/Codespaces environments + if (process.platform === "linux" || isCodespacesEnvironment()) { + args.push("--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage") + } + + this.browser = await stats.puppeteer.launch({ + args, + executablePath: stats.executablePath, + defaultViewport: this.getViewport(), + // headless: false, + }) + this.isUsingRemoteBrowser = false + } catch (error) { + // Check if this is a missing dependency error in Codespaces + if (isCodespacesEnvironment() && isMissingDependencyError(error)) { + console.log("Browser launch failed due to missing dependencies, attempting to fix...") + + // Try to fix the dependencies + const fixed = await fixCodespaceDependencies() + + if (fixed) { + // Retry launching after fixing dependencies + console.log("Dependencies fixed, retrying browser launch...") + const stats = await this.ensureChromiumExists() + + 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", + "--no-sandbox", + "--disable-setuid-sandbox", + "--disable-dev-shm-usage", + ] + + this.browser = await stats.puppeteer.launch({ + args, + executablePath: stats.executablePath, + defaultViewport: this.getViewport(), + // headless: false, + }) + this.isUsingRemoteBrowser = false + return + } + } + + // If we couldn't fix it or it's not a Codespaces issue, throw the original error + throw error + } } /** diff --git a/src/services/browser/UrlContentFetcher.ts b/src/services/browser/UrlContentFetcher.ts index b271bc2ef4..7ead279ce6 100644 --- a/src/services/browser/UrlContentFetcher.ts +++ b/src/services/browser/UrlContentFetcher.ts @@ -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 { isCodespacesEnvironment, fixCodespaceDependencies, isMissingDependencyError } from "./codespaceUtils" // Timeout constants const URL_FETCH_TIMEOUT = 30_000 // 30 seconds @@ -37,44 +38,115 @@ export class UrlContentFetcher { if (!dirExists) { await fs.mkdir(puppeteerDir, { recursive: true }) } - // if chromium doesn't exist, this will download it to path.join(puppeteerDir, ".chromium-browser-snapshots") - // if it does exist it will return the path to existing chromium - const stats: PCRStats = await PCR({ - downloadPath: puppeteerDir, - }) - return stats + + try { + // if chromium doesn't exist, this will download it to path.join(puppeteerDir, ".chromium-browser-snapshots") + // if it does exist it will return the path to existing chromium + const stats: PCRStats = await PCR({ + downloadPath: puppeteerDir, + }) + return stats + } catch (error) { + // Check if this is a missing dependency error in Codespaces + if (isCodespacesEnvironment() && isMissingDependencyError(error)) { + console.log("Detected missing browser dependencies in Codespaces, attempting to fix...") + + // Try to fix the dependencies + const fixed = await fixCodespaceDependencies() + + if (fixed) { + // Retry PCR after fixing dependencies + console.log("Dependencies fixed, retrying browser initialization...") + const stats: PCRStats = await PCR({ + downloadPath: puppeteerDir, + }) + return stats + } + } + + // If we couldn't fix it or it's not a Codespaces issue, throw the original error + throw error + } } async launchBrowser(): Promise { if (this.browser) { return } - const stats = await this.ensureChromiumExists() - 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", - "--disable-accelerated-2d-canvas", - "--no-first-run", - "--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") - } - this.browser = await stats.puppeteer.launch({ - args, - executablePath: stats.executablePath, - }) - // (latest version of puppeteer does not add headless to user agent) - this.page = await this.browser?.newPage() - - // Set additional page configurations to improve loading success - if (this.page) { - await this.page.setViewport({ width: 1280, height: 720 }) - await this.page.setExtraHTTPHeaders({ - "Accept-Language": "en-US,en;q=0.9", + + try { + const stats = await this.ensureChromiumExists() + 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", + "--disable-accelerated-2d-canvas", + "--no-first-run", + "--disable-gpu", + "--disable-features=VizDisplayCompositor", + ] + + // Add additional args for Linux/Codespaces environments + if (process.platform === "linux" || isCodespacesEnvironment()) { + args.push("--no-sandbox", "--disable-setuid-sandbox") + } + + this.browser = await stats.puppeteer.launch({ + args, + executablePath: stats.executablePath, }) + // (latest version of puppeteer does not add headless to user agent) + this.page = await this.browser?.newPage() + + // Set additional page configurations to improve loading success + if (this.page) { + await this.page.setViewport({ width: 1280, height: 720 }) + await this.page.setExtraHTTPHeaders({ + "Accept-Language": "en-US,en;q=0.9", + }) + } + } catch (error) { + // Check if this is a missing dependency error in Codespaces + if (isCodespacesEnvironment() && isMissingDependencyError(error)) { + console.log("Browser launch failed due to missing dependencies, attempting to fix...") + + // Try to fix the dependencies + const fixed = await fixCodespaceDependencies() + + if (fixed) { + // Retry launching after fixing dependencies + console.log("Dependencies fixed, retrying browser launch...") + const stats = await this.ensureChromiumExists() + 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", + "--disable-accelerated-2d-canvas", + "--no-first-run", + "--disable-gpu", + "--disable-features=VizDisplayCompositor", + "--no-sandbox", + "--disable-setuid-sandbox", + ] + + this.browser = await stats.puppeteer.launch({ + args, + executablePath: stats.executablePath, + }) + // (latest version of puppeteer does not add headless to user agent) + this.page = await this.browser?.newPage() + + // Set additional page configurations to improve loading success + if (this.page) { + await this.page.setViewport({ width: 1280, height: 720 }) + await this.page.setExtraHTTPHeaders({ + "Accept-Language": "en-US,en;q=0.9", + }) + } + return + } + } + + // If we couldn't fix it or it's not a Codespaces issue, throw the original error + throw error } } diff --git a/src/services/browser/__tests__/UrlContentFetcher.spec.ts b/src/services/browser/__tests__/UrlContentFetcher.spec.ts index b21456e379..1e8687caf9 100644 --- a/src/services/browser/__tests__/UrlContentFetcher.spec.ts +++ b/src/services/browser/__tests__/UrlContentFetcher.spec.ts @@ -184,6 +184,7 @@ describe("UrlContentFetcher", () => { "--disable-gpu", "--disable-features=VizDisplayCompositor", "--no-sandbox", // Linux-specific argument + "--disable-setuid-sandbox", // Additional Linux/Codespaces argument ], executablePath: "/path/to/chromium", }) diff --git a/src/services/browser/__tests__/codespaceUtils.spec.ts b/src/services/browser/__tests__/codespaceUtils.spec.ts new file mode 100644 index 0000000000..f69189e93b --- /dev/null +++ b/src/services/browser/__tests__/codespaceUtils.spec.ts @@ -0,0 +1,148 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" + +// Mock util module before importing the module under test +vi.mock("util", () => ({ + promisify: vi.fn(() => vi.fn()), +})) + +// Import after mocking +import * as codespaceUtils from "../codespaceUtils" +import { promisify } from "util" + +describe("codespaceUtils", () => { + let originalEnv: NodeJS.ProcessEnv + let mockExecAsync: ReturnType + + beforeEach(() => { + originalEnv = { ...process.env } + vi.clearAllMocks() + + // Create a new mock for each test + mockExecAsync = vi.fn() + vi.mocked(promisify).mockReturnValue(mockExecAsync) + }) + + afterEach(() => { + process.env = originalEnv + }) + + describe("isCodespacesEnvironment", () => { + it("should return true when CODESPACES env var is 'true'", () => { + process.env.CODESPACES = "true" + expect(codespaceUtils.isCodespacesEnvironment()).toBe(true) + }) + + it("should return true when GITHUB_CODESPACE_TOKEN is defined", () => { + process.env.GITHUB_CODESPACE_TOKEN = "some-token" + expect(codespaceUtils.isCodespacesEnvironment()).toBe(true) + }) + + it("should return false when neither env var is set", () => { + delete process.env.CODESPACES + delete process.env.GITHUB_CODESPACE_TOKEN + expect(codespaceUtils.isCodespacesEnvironment()).toBe(false) + }) + }) + + describe("isMissingDependencyError", () => { + it("should detect libatk dependency errors", () => { + const error = new Error( + "error while loading shared libraries: libatk-1.0.so.0: cannot open shared object file", + ) + expect(codespaceUtils.isMissingDependencyError(error)).toBe(true) + }) + + it("should detect Failed to launch browser errors", () => { + const error = new Error("Failed to launch the browser process!") + expect(codespaceUtils.isMissingDependencyError(error)).toBe(true) + }) + + it("should detect various missing library errors", () => { + const libraries = [ + "libatk-bridge", + "libatspi", + "libcups", + "libdbus", + "libdrm", + "libgbm", + "libgtk", + "libnspr", + "libnss", + "libx11-xcb", + "libxcomposite", + "libxdamage", + "libxfixes", + "libxkbcommon", + "libxrandr", + ] + + libraries.forEach((lib) => { + const error = new Error(`Missing ${lib} library`) + expect(codespaceUtils.isMissingDependencyError(error)).toBe(true) + }) + }) + + it("should return false for non-dependency errors", () => { + const error = new Error("Some other error") + expect(codespaceUtils.isMissingDependencyError(error)).toBe(false) + }) + + it("should handle null/undefined errors gracefully", () => { + expect(codespaceUtils.isMissingDependencyError(null)).toBe(false) + expect(codespaceUtils.isMissingDependencyError(undefined)).toBe(false) + }) + }) + + describe("fixCodespaceDependencies", () => { + it("should return false when not in Codespaces environment", async () => { + delete process.env.CODESPACES + delete process.env.GITHUB_CODESPACE_TOKEN + + const result = await codespaceUtils.fixCodespaceDependencies() + expect(result).toBe(false) + }) + + it("should attempt to fix dependencies in Codespaces", async () => { + process.env.CODESPACES = "true" + + mockExecAsync + .mockResolvedValueOnce({ stdout: "", stderr: "" }) // apt --fix-broken install + .mockResolvedValueOnce({ stdout: "/usr/bin/chromium-browser", stderr: "" }) // which chromium + + const result = await codespaceUtils.fixCodespaceDependencies() + + expect(result).toBe(true) + expect(mockExecAsync).toHaveBeenCalledWith( + "sudo apt --fix-broken install -y", + expect.objectContaining({ timeout: 60000 }), + ) + }) + + it("should install chromium if not found", async () => { + process.env.CODESPACES = "true" + + mockExecAsync + .mockResolvedValueOnce({ stdout: "", stderr: "" }) // apt --fix-broken install + .mockRejectedValueOnce(new Error("Command not found")) // which chromium (fails) + .mockResolvedValueOnce({ stdout: "", stderr: "" }) // apt-get install chromium + + const result = await codespaceUtils.fixCodespaceDependencies() + + expect(result).toBe(true) + expect(mockExecAsync).toHaveBeenCalledWith( + "sudo apt-get update && sudo apt-get install -y chromium-browser", + expect.objectContaining({ timeout: 120000 }), + ) + }) + + it("should handle errors gracefully", async () => { + process.env.CODESPACES = "true" + + mockExecAsync.mockRejectedValue(new Error("Permission denied")) + + const result = await codespaceUtils.fixCodespaceDependencies() + + expect(result).toBe(false) + }) + }) +}) diff --git a/src/services/browser/codespaceUtils.ts b/src/services/browser/codespaceUtils.ts new file mode 100644 index 0000000000..fbb980ae03 --- /dev/null +++ b/src/services/browser/codespaceUtils.ts @@ -0,0 +1,91 @@ +import { exec } from "child_process" +import { promisify } from "util" + +const execAsync = promisify(exec) + +/** + * Detects if we're running in a GitHub Codespaces environment + */ +export function isCodespacesEnvironment(): boolean { + return process.env.CODESPACES === "true" || process.env.GITHUB_CODESPACE_TOKEN !== undefined +} + +/** + * Attempts to fix missing browser dependencies in Codespaces + * by running `sudo apt --fix-broken install` + */ +export async function fixCodespaceDependencies(): Promise { + if (!isCodespacesEnvironment()) { + return false + } + + console.log("Detected Codespaces environment. Attempting to fix missing browser dependencies...") + + try { + // Run the fix command as suggested by the user + const { stdout, stderr } = await execAsync("sudo apt --fix-broken install -y", { + timeout: 60000, // 60 second timeout + }) + + if (stderr && !stderr.includes("0 upgraded, 0 newly installed")) { + console.log("apt --fix-broken install stderr:", stderr) + } + + console.log("Successfully ran apt --fix-broken install") + + // Also ensure chromium is installed + const { stdout: chromiumCheck } = await execAsync( + "which chromium-browser || which chromium || which google-chrome", + { + timeout: 5000, + }, + ).catch(() => ({ stdout: "" })) + + if (!chromiumCheck.trim()) { + console.log("Chromium not found, attempting to install...") + await execAsync("sudo apt-get update && sudo apt-get install -y chromium-browser", { + timeout: 120000, // 2 minute timeout for installation + }) + console.log("Chromium installation completed") + } + + return true + } catch (error) { + console.error("Failed to fix Codespace dependencies:", error) + return false + } +} + +/** + * Checks if the error is related to missing browser dependencies + */ +export function isMissingDependencyError(error: any): boolean { + const errorString = error?.toString() || "" + const errorMessage = error?.message || "" + + const dependencyPatterns = [ + "libatk-1.0.so.0", + "libatk-bridge", + "libatspi", + "libcups", + "libdbus", + "libdrm", + "libgbm", + "libgtk", + "libnspr", + "libnss", + "libx11-xcb", + "libxcomposite", + "libxdamage", + "libxfixes", + "libxkbcommon", + "libxrandr", + "cannot open shared object file", + "error while loading shared libraries", + "Failed to launch the browser process", + "No such file or directory", + ] + + const combinedError = `${errorString} ${errorMessage}`.toLowerCase() + return dependencyPatterns.some((pattern) => combinedError.includes(pattern.toLowerCase())) +}