diff options
Diffstat (limited to 'extension/src/activation/environmentSetup.ts')
-rw-r--r-- | extension/src/activation/environmentSetup.ts | 569 |
1 files changed, 132 insertions, 437 deletions
diff --git a/extension/src/activation/environmentSetup.ts b/extension/src/activation/environmentSetup.ts index 50a2783a..db457bd2 100644 --- a/extension/src/activation/environmentSetup.ts +++ b/extension/src/activation/environmentSetup.ts @@ -10,67 +10,7 @@ import * as vscode from "vscode"; import * as os from "os"; import fkill from "fkill"; -const WINDOWS_REMOTE_SIGNED_SCRIPTS_ERROR = - "A Python virtual enviroment cannot be activated because running scripts is disabled for this user. In order to use Continue, please enable signed scripts to run with this command in PowerShell: `Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser`, reload VS Code, and then try again."; - -const MAX_RETRIES = 3; -async function retryThenFail( - fn: () => Promise<any>, - retries: number = MAX_RETRIES -): Promise<any> { - try { - if (retries < MAX_RETRIES && process.platform === "win32") { - let [stdout, stderr] = await runCommand("Get-ExecutionPolicy"); - if (!stdout.includes("RemoteSigned")) { - [stdout, stderr] = await runCommand( - "Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser" - ); - console.log("Execution policy stdout: ", stdout); - console.log("Execution policy stderr: ", stderr); - } - } - - return await fn(); - } catch (e: any) { - if (retries > 0) { - return await retryThenFail(fn, retries - 1); - } - - // Show corresponding error message depending on the platform - let msg = - "Failed to set up Continue extension. Please email hi@continue.dev and we'll get this fixed ASAP!"; - try { - switch (process.platform) { - case "win32": - msg = WINDOWS_REMOTE_SIGNED_SCRIPTS_ERROR; - break; - case "darwin": - break; - case "linux": - const [pythonCmd] = await getPythonPipCommands(); - msg = await getLinuxAptInstallError(pythonCmd); - break; - } - } finally { - console.log("After retries, failed to set up Continue extension", msg); - vscode.window - .showErrorMessage(msg, "View Logs", "Retry") - .then((selection) => { - if (selection === "View Logs") { - vscode.commands.executeCommand("continue.viewLogs"); - } else if (selection === "Retry") { - // Reload VS Code window - vscode.commands.executeCommand("workbench.action.reloadWindow"); - } - }); - } - - throw e; - } -} - async function runCommand(cmd: string): Promise<[string, string | undefined]> { - console.log("Running command: ", cmd); var stdout: any = ""; var stderr: any = ""; try { @@ -91,276 +31,10 @@ async function runCommand(cmd: string): Promise<[string, string | undefined]> { return [stdout, stderr]; } -export async function getPythonPipCommands() { - var [stdout, stderr] = await runCommand("python3 --version"); - let pythonCmd = "python3"; - if (stderr) { - // If not, first see if python3 is aliased to python - var [stdout, stderr] = await runCommand("python --version"); - if ( - (typeof stderr === "undefined" || stderr === "") && - stdout.split(" ")[1][0] === "3" - ) { - // Python3 is aliased to python - pythonCmd = "python"; - } else { - // Python doesn't exist at all - vscode.window.showErrorMessage( - "Continue requires Python3. Please install from https://www.python.org/downloads, reload VS Code, and try again." - ); - throw new Error("Python 3 is not installed."); - } - } - - let pipCmd = pythonCmd.endsWith("3") ? "pip3" : "pip"; - - const version = stdout.split(" ")[1]; - const [major, minor] = version.split("."); - if (parseInt(major) !== 3 || parseInt(minor) < 8) { - // Need to check specific versions - const checkPython3VersionExists = async (minorVersion: number) => { - const [stdout, stderr] = await runCommand( - `python3.${minorVersion} --version` - ); - return typeof stderr === "undefined" || stderr === ""; - }; - - const VALID_VERSIONS = [8, 9, 10, 11, 12]; - let versionExists = false; - - for (const minorVersion of VALID_VERSIONS) { - if (await checkPython3VersionExists(minorVersion)) { - versionExists = true; - pythonCmd = `python3.${minorVersion}`; - pipCmd = `pip3.${minorVersion}`; - } - } - - if (!versionExists) { - vscode.window.showErrorMessage( - "Continue requires Python version 3.8 or greater. Please update your Python installation, reload VS Code, and try again." - ); - throw new Error("Python3.8 or greater is not installed."); - } - } - - return [pythonCmd, pipCmd]; -} - -function getActivateUpgradeCommands(pythonCmd: string, pipCmd: string) { - let activateCmd = ". env/bin/activate"; - let pipUpgradeCmd = `${pipCmd} install --upgrade pip`; - if (process.platform == "win32") { - activateCmd = ".\\env\\Scripts\\activate"; - pipUpgradeCmd = `${pythonCmd} -m pip install --upgrade pip`; - } - return [activateCmd, pipUpgradeCmd]; -} - -function checkEnvExists() { - const envBinPath = path.join( - serverPath(), - "env", - process.platform == "win32" ? "Scripts" : "bin" - ); - return ( - fs.existsSync(path.join(envBinPath, "activate")) && - fs.existsSync( - path.join(envBinPath, process.platform == "win32" ? "pip.exe" : "pip") - ) - ); -} - -async function checkRequirementsInstalled() { - // First, check if the requirements have been installed most recently for a later version of the extension - if (fs.existsSync(requirementsVersionPath())) { - const requirementsVersion = fs.readFileSync( - requirementsVersionPath(), - "utf8" - ); - if (requirementsVersion !== getExtensionVersion()) { - // Remove the old version of continuedev from site-packages - const [pythonCmd, pipCmd] = await getPythonPipCommands(); - const [activateCmd] = getActivateUpgradeCommands(pythonCmd, pipCmd); - const removeOldVersionCommand = [ - `cd "${serverPath()}"`, - activateCmd, - `${pipCmd} uninstall -y continuedev`, - ].join(" ; "); - await runCommand(removeOldVersionCommand); - return false; - } - } - - let envLibsPath = path.join( - serverPath(), - "env", - process.platform == "win32" ? "Lib" : "lib" - ); - // If site-packages is directly under env, use that - if (fs.existsSync(path.join(envLibsPath, "site-packages"))) { - envLibsPath = path.join(envLibsPath, "site-packages"); - } else { - // Get the python version folder name - const pythonVersions = fs.readdirSync(envLibsPath).filter((f: string) => { - return f.startsWith("python"); - }); - if (pythonVersions.length == 0) { - return false; - } - const pythonVersion = pythonVersions[0]; - envLibsPath = path.join(envLibsPath, pythonVersion, "site-packages"); - } - - const continuePath = path.join(envLibsPath, "continuedev"); - - return fs.existsSync(continuePath); -} - -async function getLinuxAptInstallError(pythonCmd: string) { - // First, try to run the command to install python3-venv - let [stdout, stderr] = await runCommand(`${pythonCmd} --version`); - if (stderr) { - await vscode.window.showErrorMessage( - "Python3 is not installed. Please install from https://www.python.org/downloads, reload VS Code, and try again." - ); - throw new Error(stderr); - } - const version = stdout.split(" ")[1].split(".")[1]; - const installVenvCommand = `apt-get install python3.${version}-venv`; - await runCommand("apt-get update"); - return `[Important] Continue needs to create a Python virtual environment, but python3.${version}-venv is not installed. Please run this command in your terminal: \`${installVenvCommand}\`, reload VS Code, and then try again.`; -} - -async function createPythonVenv(pythonCmd: string) { - if (checkEnvExists()) { - console.log("Python env already exists, skipping..."); - } else { - // Assemble the command to create the env - const createEnvCommand = [ - `cd "${serverPath()}"`, - `${pythonCmd} -m venv env`, - ].join(" ; "); - - const [stdout, stderr] = await runCommand(createEnvCommand); - if ( - stderr && - stderr.includes("running scripts is disabled on this system") - ) { - console.log("Scripts disabled error when trying to create env"); - await vscode.window.showErrorMessage(WINDOWS_REMOTE_SIGNED_SCRIPTS_ERROR); - throw new Error(stderr); - } else if ( - stderr?.includes("On Debian/Ubuntu systems") || - stdout?.includes("On Debian/Ubuntu systems") - ) { - const msg = await getLinuxAptInstallError(pythonCmd); - console.log(msg); - await vscode.window.showErrorMessage(msg); - } else if (checkEnvExists()) { - console.log("Successfully set up python env at ", `${serverPath()}/env`); - } else if ( - stderr?.includes("Permission denied") && - stderr?.includes("python.exe") - ) { - // This might mean that another window is currently using the python.exe file to install requirements - // So we want to wait and try again - let i = 0; - await new Promise((resolve, reject) => - setInterval(() => { - if (i > 5) { - reject("Timed out waiting for other window to create env..."); - } - if (checkEnvExists()) { - resolve(null); - } else { - console.log("Waiting for other window to create env..."); - } - i++; - }, 5000) - ); - } else { - const msg = [ - "Python environment not successfully created. Trying again. Here was the stdout + stderr: ", - `stdout: ${stdout}`, - `stderr: ${stderr}`, - ].join("\n\n"); - console.log(msg); - throw new Error(msg); - } - } -} - -async function setupPythonEnv() { - console.log("Setting up python env for Continue extension..."); - - const [pythonCmd, pipCmd] = await getPythonPipCommands(); - const [activateCmd, pipUpgradeCmd] = getActivateUpgradeCommands( - pythonCmd, - pipCmd - ); - - await retryThenFail(async () => { - // First, create the virtual environment - await createPythonVenv(pythonCmd); - - // Install the requirements - if (await checkRequirementsInstalled()) { - console.log("Python requirements already installed, skipping..."); - } else { - const installRequirementsCommand = [ - `cd "${serverPath()}"`, - activateCmd, - pipUpgradeCmd, - `${pipCmd} install -r requirements.txt`, - ].join(" ; "); - const [, stderr] = await runCommand(installRequirementsCommand); - if (stderr) { - throw new Error(stderr); - } - // Write the version number for which requirements were installed - fs.writeFileSync(requirementsVersionPath(), getExtensionVersion()); - } - }); -} - -function readEnvFile(path: string) { - if (!fs.existsSync(path)) { - return {}; - } - let envFile = fs.readFileSync(path, "utf8"); - - let env: { [key: string]: string } = {}; - envFile.split("\n").forEach((line) => { - let [key, value] = line.split("="); - if (typeof key === "undefined" || typeof value === "undefined") { - return; - } - env[key] = value.replace(/"/g, ""); - }); - return env; -} - -function writeEnvFile(path: string, key: string, value: string) { - if (!fs.existsSync(path)) { - fs.writeFileSync(path, `${key}="${value}"`); - return; - } - - let env = readEnvFile(path); - env[key] = value; - - let newEnvFile = ""; - for (let key in env) { - newEnvFile += `${key}="${env[key]}"\n`; - } - fs.writeFileSync(path, newEnvFile); -} - async function checkServerRunning(serverUrl: string): Promise<boolean> { // Check if already running by calling /health try { - const response = await fetch(serverUrl + "/health"); + const response = await fetch(`${serverUrl}/health`); if (response.status === 200) { console.log("Continue python server already running"); return true; @@ -381,16 +55,6 @@ export function getContinueGlobalPath(): string { return continuePath; } -function setupServerPath() { - const sPath = serverPath(); - const extensionServerPath = path.join(getExtensionUri().fsPath, "server"); - const files = fs.readdirSync(extensionServerPath); - files.forEach((file) => { - const filePath = path.join(extensionServerPath, file); - fs.copyFileSync(filePath, path.join(sPath, file)); - }); -} - function serverPath(): string { const sPath = path.join(getContinueGlobalPath(), "server"); if (!fs.existsSync(sPath)) { @@ -411,37 +75,24 @@ function serverVersionPath(): string { return path.join(serverPath(), "server_version.txt"); } -function requirementsVersionPath(): string { - return path.join(serverPath(), "requirements_version.txt"); -} - export function getExtensionVersion() { const extension = vscode.extensions.getExtension("continue.continue"); return extension?.packageJSON.version || ""; } -export async function startContinuePythonServer() { - // Check vscode settings - const serverUrl = getContinueServerUrl(); - if (serverUrl !== "http://localhost:65432") { - return; - } - - setupServerPath(); - - return await retryThenFail(async () => { - console.log("Checking if server is old version"); - // Kill the server if it is running an old version - if (fs.existsSync(serverVersionPath())) { - const serverVersion = fs.readFileSync(serverVersionPath(), "utf8"); - if ( - serverVersion === getExtensionVersion() && - (await checkServerRunning(serverUrl)) - ) { - // The current version is already up and running, no need to continue - return; - } +// Returns whether a server of the current version is already running +async function checkOrKillRunningServer(serverUrl: string): Promise<boolean> { + console.log("Checking if server is old version"); + const serverRunning = await checkServerRunning(serverUrl); + // Kill the server if it is running an old version + if (fs.existsSync(serverVersionPath())) { + const serverVersion = fs.readFileSync(serverVersionPath(), "utf8"); + if (serverVersion === getExtensionVersion() && serverRunning) { + // The current version is already up and running, no need to continue + return true; } + } + if (serverRunning) { console.log("Killing old server..."); try { await fkill(":65432"); @@ -450,92 +101,136 @@ export async function startContinuePythonServer() { console.log("Failed to kill old server:", e); } } + } + return false; +} - // Do this after above check so we don't have to waste time setting up the env - await setupPythonEnv(); +function ensureDirectoryExistence(filePath: string) { + const dirname = path.dirname(filePath); + if (fs.existsSync(dirname)) { + return true; + } + ensureDirectoryExistence(dirname); + fs.mkdirSync(dirname); +} - // Spawn the server process on port 65432 - const [pythonCmd] = await getPythonPipCommands(); - const activateCmd = - process.platform == "win32" - ? ".\\env\\Scripts\\activate" - : ". env/bin/activate"; +export async function downloadFromS3( + bucket: string, + fileName: string, + destination: string, + region: string +) { + const s3Url = `https://${bucket}.s3.${region}.amazonaws.com/${fileName}`; + const response = await fetch(s3Url, { + method: "GET", + }); + if (!response.ok) { + const text = await response.text(); + const errText = `Failed to download Continue server from S3: ${text}`; + vscode.window.showErrorMessage(errText); + throw new Error(errText); + } + const buffer = await response.buffer(); + ensureDirectoryExistence(destination); + fs.writeFileSync(destination, buffer); +} - const command = `cd "${serverPath()}" && ${activateCmd} && cd .. && ${pythonCmd} -m server.run_continue_server`; +export async function startContinuePythonServer() { + // Check vscode settings + const serverUrl = getContinueServerUrl(); + if (serverUrl !== "http://localhost:65432") { + console.log("Continue server is being run manually, skipping start"); + return; + } - return new Promise(async (resolve, reject) => { - console.log("Starting Continue python server..."); - try { - const child = spawn(command, { - shell: true, - }); - child.stderr.on("data", (data: any) => { - if ( - data.includes("Uvicorn running on") || // Successfully started the server - data.includes("only one usage of each socket address") || // [windows] The server is already running (probably a simultaneously opened VS Code window) - data.includes("address already in use") // [mac/linux] The server is already running (probably a simultaneously opened VS Code window) - ) { - console.log("Successfully started Continue python server"); - resolve(null); - } else if (data.includes("ERROR") || data.includes("Traceback")) { - console.log("Error starting Continue python server: ", data); - } else { - console.log(`stdout: ${data}`); - } - }); - child.on("error", (error: any) => { - console.log(`error: ${error.message}`); - }); + // Check if server is already running + if (await checkOrKillRunningServer(serverUrl)) { + console.log("Continue server already running"); + return; + } - child.on("close", (code: any) => { - console.log(`child process exited with code ${code}`); - }); + // Download the server executable + const bucket = "continue-server-binaries"; + const fileName = + os.platform() === "win32" + ? "windows/run.exe" + : os.platform() === "darwin" + ? "mac/run" + : "linux/run"; + + const destination = path.join( + getExtensionUri().fsPath, + "server", + "exe", + `run${os.platform() === "win32" ? ".exe" : ""}` + ); - child.stdout.on("data", (data: any) => { - console.log(`stdout: ${data}`); - }); + // First, check if the server is already downloaded + let shouldDownload = true; + if (fs.existsSync(destination)) { + // Check if the server is the correct version + const serverVersion = fs.readFileSync(serverVersionPath(), "utf8"); + if (serverVersion === getExtensionVersion()) { + // The current version is already up and running, no need to continue + console.log("Continue server already downloaded"); + shouldDownload = false; + } + } - // Write the current version of vscode to a file called server_version.txt - fs.writeFileSync(serverVersionPath(), getExtensionVersion()); - } catch (e) { - console.log("Failed to start Continue python server", e); - // If failed, check if it's because the server is already running (might have happened just after we checked above) - if (await checkServerRunning(serverUrl)) { - resolve(null); - } else { - reject(); - } + if (shouldDownload) { + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: "Installing Continue server...", + cancellable: false, + }, + async () => { + await downloadFromS3(bucket, fileName, destination, "us-west-1"); } - }); - }); -} - -export function isPythonEnvSetup(): boolean { - const pathToEnvCfg = path.join(serverPath(), "env", "pyvenv.cfg"); - return fs.existsSync(pathToEnvCfg); -} + ); + } -export async function downloadPython3() { - // Download python3 and return the command to run it (python or python3) - let os = process.platform; - let command: string = ""; - let pythonCmd = "python3"; - if (os === "darwin") { - throw new Error("python3 not found"); - } else if (os === "linux") { - command = - "sudo apt update ; upgrade ; sudo apt install python3 python3-pip"; - } else if (os === "win32") { - command = - "wget -O python_installer.exe https://www.python.org/ftp/python/3.11.3/python-3.11.3-amd64.exe ; python_installer.exe /quiet InstallAllUsers=1 PrependPath=1 Include_test=0"; - pythonCmd = "python"; + console.log("Downloaded server executable at ", destination); + // Get name of the corresponding executable for platform + if (os.platform() === "darwin") { + // Add necessary permissions + console.log("Setting permissions for Continue server..."); + fs.chmodSync(destination, 0o7_5_5); + const [stdout1, stderr1] = await runCommand( + `xattr -dr com.apple.quarantine ${destination}` + ); + console.log("stdout: ", stdout1); + console.log("stderr: ", stderr1); } - var [stdout, stderr] = await runCommand(command); - if (stderr) { - throw new Error(stderr); + // Validate that the file exists + if (!fs.existsSync(destination)) { + const errText = `- Failed to install Continue server.`; + vscode.window.showErrorMessage(errText); + throw new Error(errText); } - console.log("Successfully downloaded python3"); - return pythonCmd; + // Run the executable + console.log("Starting Continue server..."); + const child = spawn(destination, { + shell: true, + }); + child.stderr.on("data", (data: any) => { + console.log(data.toString()); + }); + + child.on("error", (error: any) => { + console.log(`error: ${error.message}`); + }); + + child.on("close", (code: any) => { + console.log(`child process exited with code ${code}`); + }); + + child.stdout.on("data", (data: any) => { + console.log(`stdout: ${data.toString()}`); + }); + + // Write the current version of vscode extension to a file called server_version.txt + fs.writeFileSync(serverVersionPath(), getExtensionVersion()); } |