diff --git a/extensions/codestory/src/utilities/gcpBucket.ts b/extensions/codestory/src/utilities/gcpBucket.ts index 76511e998db..7949ace9e0e 100644 --- a/extensions/codestory/src/utilities/gcpBucket.ts +++ b/extensions/codestory/src/utilities/gcpBucket.ts @@ -22,31 +22,66 @@ function ensureDirectoryExists(filePath: string): void { } export const downloadSidecarZip = async ( - destination: string, - version: string = 'latest' + destination: string, + version: string = 'latest' ) => { - ensureDirectoryExists(destination); + try { + ensureDirectoryExists(destination); - const platform = process.platform; - const architecture = process.arch; - const source = `${version}/${platform}/${architecture}/sidecar.zip`; - try { - await downloadUsingURL(source, destination); - } catch (err) { - console.error(err); - throw new Error(`Failed to download sidecar`); - } + const platform = process.platform; + const architecture = process.arch; + const source = `${version}/${platform}/${architecture}/sidecar.zip`; + console.log(`Downloading sidecar for ${platform}-${architecture} from version ${version}`); + + await downloadUsingURL(source, destination); + console.log('Successfully downloaded sidecar binary'); + } catch (err) { + console.error('Failed to download sidecar:', err); + if (err.response) { + console.error('Response status:', err.response.status); + console.error('Response data:', err.response.data); + } + throw new Error(`Failed to download sidecar: ${err.message}`); + } }; const downloadUsingURL = async (source: string, destination: string) => { - const url = `https://storage.googleapis.com/${BUCKET_NAME}/${source}`; - const response = await axios.get(url, { responseType: 'stream' }); - const writer = fs.createWriteStream(destination); + const url = `https://storage.googleapis.com/${BUCKET_NAME}/${source}`; + console.log('Downloading from URL:', url); + + try { + const response = await axios.get(url, { + responseType: 'stream', + timeout: 30000 // 30 second timeout + }); + + const writer = fs.createWriteStream(destination); - response.data.pipe(writer); - - return new Promise((resolve, reject) => { - writer.on('finish', resolve); - writer.on('error', reject); - }); -}; + return new Promise((resolve, reject) => { + response.data.pipe(writer); + + let error: Error | null = null; + writer.on('error', err => { + error = err; + writer.close(); + reject(err); + }); + + writer.on('close', () => { + if (!error) { + resolve(true); + } + // No need to reject here as it would have been handled in the error handler + }); + }); + } catch (err) { + if (err.code === 'ECONNREFUSED') { + throw new Error('Connection refused. Please check your internet connection.'); + } else if (err.code === 'ETIMEDOUT') { + throw new Error('Connection timed out. Please try again.'); + } else if (err.response && err.response.status === 404) { + throw new Error(`Sidecar binary not found for your platform (${process.platform}-${process.arch}). Please check https://aide-updates.codestory.ai for supported platforms.`); + } + throw err; + } +}; \ No newline at end of file diff --git a/extensions/codestory/src/utilities/setupSidecarBinary.ts b/extensions/codestory/src/utilities/setupSidecarBinary.ts index 255f2286b28..09c212184a7 100644 --- a/extensions/codestory/src/utilities/setupSidecarBinary.ts +++ b/extensions/codestory/src/utilities/setupSidecarBinary.ts @@ -57,16 +57,22 @@ async function getHealthCheckURL(): Promise { } async function healthCheck(): Promise { - try { - const healthCheckURL = await getHealthCheckURL(); - const response = await fetch(healthCheckURL); - const isHealthy = response.status === 200; - vscode.sidecar.setRunningStatus(vscode.SidecarRunningStatus.Connected); - return isHealthy; - } catch (e) { - console.error('Health check failed with error:', e); - return false; - } + try { + const healthCheckURL = await getHealthCheckURL(); + console.log('Performing health check at:', healthCheckURL); + const response = await fetch(healthCheckURL); + const isHealthy = response.status === 200; + if (!isHealthy) { + console.error('Health check failed with status:', response.status); + console.error('Health check response:', await response.text()); + } + vscode.sidecar.setRunningStatus(isHealthy ? vscode.SidecarRunningStatus.Connected : vscode.SidecarRunningStatus.Unavailable); + return isHealthy; + } catch (e) { + console.error('Health check failed with error:', e); + vscode.sidecar.setRunningStatus(vscode.SidecarRunningStatus.Unavailable); + return false; + } } type VersionAPIResponse = { @@ -338,108 +344,122 @@ export async function restartSidecarBinary(extensionBasePath: string) { } export async function setupSidecar(extensionBasePath: string): Promise { - const { zipDestination, extractedDestination, webserverPath } = getPaths(extensionBasePath); - - // If user is self-managing sidecar, only do health checks - if (sidecarUseSelfRun()) { - console.log('User is self-managing sidecar binary, skipping automated setup'); - const hc = await healthCheck(); - if (!hc) { - console.log('Sidecar health check failed'); - vscode.sidecar.setRunningStatus(vscode.SidecarRunningStatus.Unavailable); - vscode.window.showWarningMessage('Sidecar is not running. Please start the sidecar binary manually as configured.'); - } - - // Set up recurring health check every 5 seconds - const healthCheckInterval = setInterval(async () => { - const isHealthy = await healthCheck(); - if (!isHealthy) { - console.log('Sidecar health check failed'); - vscode.sidecar.setRunningStatus(vscode.SidecarRunningStatus.Unavailable); - } else { - versionCheck(); - } - }, 5000); - - return vscode.Disposable.from({ dispose: () => clearInterval(healthCheckInterval) }); - } - - // Regular automated setup flow - if (!fs.existsSync(webserverPath)) { - vscode.sidecar.setRunningStatus(vscode.SidecarRunningStatus.Starting); - try { - await fetchSidecarWithProgress(zipDestination); - await unzipSidecarArchive(zipDestination, extractedDestination, webserverPath); - } catch (error) { - console.error('Failed to set up sidecar binary:', error); - vscode.sidecar.setRunningStatus(vscode.SidecarRunningStatus.Unavailable); - throw error; - } - } - - const hc = await healthCheck(); - if (!hc) { - await startSidecarBinary(webserverPath); - } - - // Asynchronously check for updates - checkForUpdates(zipDestination); - - // Set up recurring health check every 5 seconds to recover sidecar - const healthCheckInterval = setInterval(async () => { - // Skip health check if we're in the middle of a restart - if (isRestarting) { - console.log('Skipping health check during restart...'); - return; - } - - const isHealthy = await healthCheck(); - if (isHealthy) { - versionCheck(); - } else { - console.log('Health check failed, attempting recovery...'); - // Set to Connecting first to indicate we're trying to reconnect - vscode.sidecar.setRunningStatus(vscode.SidecarRunningStatus.Connecting); - - // First try: Attempt to restart using existing binary - try { - console.log('Attempting to restart sidecar with existing binary...'); - await restartSidecarBinary(extensionBasePath); - const recoveryCheck = await retryHealthCheck(3, 1000); - if (recoveryCheck) { - console.log('Successfully recovered sidecar using existing binary'); - return; - } - } catch (error) { - console.log('Failed to restart with existing binary:', error); - } - - // Second try: Binary might be missing, try fresh download and start - try { - console.log('Attempting fresh download and start...'); - // Kill any existing process first - await killSidecar(); - - // Fresh download and start - await fetchSidecarWithProgress(zipDestination); - await startSidecarBinary(webserverPath); - - const freshStartCheck = await retryHealthCheck(3, 1000); - if (freshStartCheck) { - console.log('Successfully recovered sidecar with fresh download'); - return; - } - } catch (error) { - console.error('Failed to recover sidecar after fresh download:', error); - } - - // If we get here, all recovery attempts failed - console.error('All recovery attempts failed'); - vscode.sidecar.setRunningStatus(vscode.SidecarRunningStatus.Unavailable); - vscode.window.showErrorMessage('Failed to recover sidecar after multiple attempts. Please try restarting Aide.'); - } - }, 5000); - - // Clean up interval when extension is deactivated - return vscode.Disposable.from({ dispose: () => clearInterval(healthCheckInterval) }); -} + const { zipDestination, extractedDestination, webserverPath } = getPaths(extensionBasePath); + + // If user is self-managing sidecar, only do health checks + if (sidecarUseSelfRun()) { + console.log('User is self-managing sidecar binary, skipping automated setup'); + const hc = await healthCheck(); + if (!hc) { + console.log('Sidecar health check failed'); + vscode.sidecar.setRunningStatus(vscode.SidecarRunningStatus.Unavailable); + vscode.window.showErrorMessage('Sidecar is not running. Please check if the sidecar binary is running at the configured URL: ' + sidecarURL()); + } + + // Set up recurring health check every 5 seconds + const healthCheckInterval = setInterval(async () => { + const isHealthy = await healthCheck(); + if (!isHealthy) { + console.log('Sidecar health check failed'); + vscode.sidecar.setRunningStatus(vscode.SidecarRunningStatus.Unavailable); + vscode.window.showErrorMessage('Lost connection to sidecar. Please check if the sidecar binary is still running.'); + } else { + versionCheck(); + } + }, 5000); + + return vscode.Disposable.from({ dispose: () => clearInterval(healthCheckInterval) }); + } + + // Regular automated setup flow + if (!fs.existsSync(webserverPath)) { + vscode.sidecar.setRunningStatus(vscode.SidecarRunningStatus.Starting); + try { + console.log('Downloading sidecar binary to:', zipDestination); + await fetchSidecarWithProgress(zipDestination); + console.log('Extracting sidecar binary to:', extractedDestination); + await unzipSidecarArchive(zipDestination, extractedDestination, webserverPath); + } catch (error) { + console.error('Failed to set up sidecar binary:', error); + vscode.sidecar.setRunningStatus(vscode.SidecarRunningStatus.Unavailable); + vscode.window.showErrorMessage(`Failed to setup sidecar binary: ${error.message}. Please try restarting VS Code.`); + throw error; + } + } + + const hc = await healthCheck(); + if (!hc) { + try { + await startSidecarBinary(webserverPath); + } catch (error) { + console.error('Failed to start sidecar binary:', error); + vscode.window.showErrorMessage(`Failed to start sidecar binary: ${error.message}. Please try restarting VS Code.`); + throw error; + } + } + + // Asynchronously check for updates + checkForUpdates(zipDestination).catch(error => { + console.error('Failed to check for updates:', error); + }); + + // Set up recurring health check every 5 seconds to recover sidecar + const healthCheckInterval = setInterval(async () => { + // Skip health check if we're in the middle of a restart + if (isRestarting) { + console.log('Skipping health check during restart...'); + return; + } + + const isHealthy = await healthCheck(); + if (isHealthy) { + versionCheck(); + } else { + console.log('Health check failed, attempting recovery...'); + // Set to Connecting first to indicate we're trying to reconnect + vscode.sidecar.setRunningStatus(vscode.SidecarRunningStatus.Connecting); + + // First try: Attempt to restart using existing binary + try { + console.log('Attempting to restart sidecar with existing binary...'); + await restartSidecarBinary(extensionBasePath); + const recoveryCheck = await retryHealthCheck(3, 1000); + if (recoveryCheck) { + console.log('Successfully recovered sidecar using existing binary'); + return; + } + } catch (error) { + console.log('Failed to restart with existing binary:', error); + } + + // Second try: Binary might be missing, try fresh download and start + try { + console.log('Attempting fresh download and start...'); + // Kill any existing process first + await killSidecar(); + + // Fresh download and start + await fetchSidecarWithProgress(zipDestination); + await unzipSidecarArchive(zipDestination, extractedDestination, webserverPath); + await startSidecarBinary(webserverPath); + + const freshStartCheck = await retryHealthCheck(3, 1000); + if (freshStartCheck) { + console.log('Successfully recovered sidecar with fresh download'); + return; + } + } catch (error) { + console.error('Failed to recover sidecar after fresh download:', error); + vscode.window.showErrorMessage('Failed to recover sidecar. Please try manually downloading the sidecar binary from https://aide-updates.codestory.ai'); + } + + // If we get here, all recovery attempts failed + console.error('All recovery attempts failed'); + vscode.sidecar.setRunningStatus(vscode.SidecarRunningStatus.Unavailable); + vscode.window.showErrorMessage('Failed to recover sidecar after multiple attempts. Please try restarting VS Code or manually downloading the sidecar binary.'); + } + }, 5000); + + // Clean up interval when extension is deactivated + return vscode.Disposable.from({ dispose: () => clearInterval(healthCheckInterval) }); +} \ No newline at end of file diff --git a/extensions/codestory/src/utilities/unzip.ts b/extensions/codestory/src/utilities/unzip.ts index fdae00daf82..1ab22805fe6 100644 --- a/extensions/codestory/src/utilities/unzip.ts +++ b/extensions/codestory/src/utilities/unzip.ts @@ -9,87 +9,142 @@ import * as path from 'path'; import * as yauzl from 'yauzl'; async function extractZipWithYauzl(zipPath: string, destinationDir: string): Promise { - return new Promise((resolve, reject) => { - yauzl.open(zipPath, { lazyEntries: true }, (err, zipfile) => { - if (err) { reject(err); return; } - if (!zipfile) { reject(new Error('Failed to open zip file')); return; } - - zipfile.on('error', reject); - zipfile.on('end', resolve); - zipfile.on('entry', async (entry) => { - const entryPath = path.join(destinationDir, entry.fileName); - const entryDir = path.dirname(entryPath); - - try { - // Create directory if it doesn't exist - fs.mkdirSync(entryDir, { recursive: true }); - - if (/\/$/.test(entry.fileName)) { - // Directory entry - fs.mkdirSync(entryPath, { recursive: true }); - zipfile.readEntry(); - } else { - // File entry - zipfile.openReadStream(entry, (err, readStream) => { - if (err) { reject(err); return; } - if (!readStream) { reject(new Error('Failed to open read stream')); return; } - - const writeStream = fs.createWriteStream(entryPath, { flags: 'w' }); - - writeStream.on('error', (error) => { - readStream.destroy(); - reject(error); - }); - - readStream.on('error', (error) => { - writeStream.destroy(); - reject(error); - }); - - readStream.pipe(writeStream); - writeStream.on('finish', () => { - zipfile.readEntry(); - }); - }); - } - } catch (error) { - reject(error); - } - }); - - zipfile.readEntry(); - }); - }); + return new Promise((resolve, reject) => { + yauzl.open(zipPath, { lazyEntries: true }, (err, zipfile) => { + if (err) { + console.error('Failed to open zip file:', err); + reject(new Error(`Failed to open zip file: ${err.message}`)); + return; + } + if (!zipfile) { + console.error('Failed to open zip file: No zipfile object returned'); + reject(new Error('Failed to open zip file')); + return; + } + + zipfile.on('error', (err) => { + console.error('Zip file error:', err); + reject(err); + }); + + zipfile.on('end', () => { + console.log('Successfully extracted all files'); + resolve(); + }); + + zipfile.on('entry', async (entry) => { + const entryPath = path.join(destinationDir, entry.fileName); + const entryDir = path.dirname(entryPath); + + try { + // Validate entry path to prevent directory traversal + const normalizedEntryPath = path.normalize(entryPath); + if (!normalizedEntryPath.startsWith(destinationDir)) { + console.error('Invalid zip entry path:', entry.fileName); + reject(new Error(`Invalid zip entry path: ${entry.fileName}`)); + return; + } + + // Create directory if it doesn't exist + fs.mkdirSync(entryDir, { recursive: true }); + + if (/\/$/.test(entry.fileName)) { + // Directory entry + fs.mkdirSync(entryPath, { recursive: true }); + zipfile.readEntry(); + } else { + // File entry + zipfile.openReadStream(entry, (err, readStream) => { + if (err) { + console.error('Failed to open read stream:', err); + reject(new Error(`Failed to open read stream: ${err.message}`)); + return; + } + if (!readStream) { + console.error('Failed to open read stream: No stream object returned'); + reject(new Error('Failed to open read stream')); + return; + } + + const writeStream = fs.createWriteStream(entryPath, { flags: 'w' }); + + writeStream.on('error', (error) => { + console.error('Write stream error:', error); + readStream.destroy(); + reject(new Error(`Failed to write file ${entry.fileName}: ${error.message}`)); + }); + + readStream.on('error', (error) => { + console.error('Read stream error:', error); + writeStream.destroy(); + reject(new Error(`Failed to read file ${entry.fileName}: ${error.message}`)); + }); + + readStream.pipe(writeStream); + writeStream.on('finish', () => { + zipfile.readEntry(); + }); + }); + } + } catch (error) { + console.error('Error processing zip entry:', error); + reject(new Error(`Failed to process zip entry ${entry.fileName}: ${error.message}`)); + } + }); + + zipfile.readEntry(); + }); + }); } export async function unzip(source: string, destinationDir: string): Promise { - if (!source || !destinationDir) { - throw new Error('Source and destination paths are required'); - } - - if (!fs.existsSync(source)) { - throw new Error(`Source file does not exist: ${source}`); - } - - try { - if (source.endsWith('.zip')) { - await extractZipWithYauzl(source, destinationDir); - } else { - // Ensure destination directory exists - if (!fs.existsSync(destinationDir)) { - fs.mkdirSync(destinationDir, { recursive: true }); - } - - const result = cp.spawnSync('tar', ['-xzf', source, '-C', destinationDir, '--strip-components', '1']); - if (result.error) { - throw result.error; - } - if (result.status !== 0) { - throw new Error(`tar extraction failed with status ${result.status}: ${result.stderr.toString()}`); - } - } - } catch (error) { - // Add context to the error before rethrowing - throw new Error(`Failed to extract ${source} to ${destinationDir}: ${error.message}`); - } -} + console.log(`Extracting ${source} to ${destinationDir}`); + + if (!source || !destinationDir) { + throw new Error('Source and destination paths are required'); + } + + if (!fs.existsSync(source)) { + throw new Error(`Source file does not exist: ${source}`); + } + + try { + // Check if destination directory is writable + try { + const testFile = path.join(destinationDir, '.write-test'); + fs.mkdirSync(destinationDir, { recursive: true }); + fs.writeFileSync(testFile, ''); + fs.unlinkSync(testFile); + } catch (error) { + throw new Error(`Destination directory is not writable: ${error.message}`); + } + + if (source.endsWith('.zip')) { + await extractZipWithYauzl(source, destinationDir); + } else { + // Ensure destination directory exists + if (!fs.existsSync(destinationDir)) { + fs.mkdirSync(destinationDir, { recursive: true }); + } + + console.log('Using tar for extraction'); + const result = cp.spawnSync('tar', ['-xzf', source, '-C', destinationDir, '--strip-components', '1']); + + if (result.error) { + console.error('Tar extraction error:', result.error); + throw result.error; + } + + if (result.status !== 0) { + const errorMessage = result.stderr.toString(); + console.error('Tar extraction failed:', errorMessage); + throw new Error(`tar extraction failed with status ${result.status}: ${errorMessage}`); + } + + console.log('Tar extraction completed successfully'); + } + } catch (error) { + console.error('Extraction error:', error); + throw new Error(`Failed to extract ${source} to ${destinationDir}: ${error.message}`); + } +} \ No newline at end of file