diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..f621d85c --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @aws/sagemaker-code-editor diff --git a/patched-vscode/build/gulpfile.extensions.js b/patched-vscode/build/gulpfile.extensions.js index 1b8bd9b3..3ec35890 100644 --- a/patched-vscode/build/gulpfile.extensions.js +++ b/patched-vscode/build/gulpfile.extensions.js @@ -62,7 +62,9 @@ const compilations = [ 'extensions/simple-browser/tsconfig.json', 'extensions/sagemaker-extension/tsconfig.json', 'extensions/sagemaker-idle-extension/tsconfig.json', + 'extensions/sagemaker-extensions-sync/tsconfig.json', 'extensions/sagemaker-terminal-crash-mitigation/tsconfig.json', + 'extensions/sagemaker-open-notebook-extension/tsconfig.json', 'extensions/tunnel-forwarding/tsconfig.json', 'extensions/typescript-language-features/test-workspace/tsconfig.json', 'extensions/typescript-language-features/web/tsconfig.json', diff --git a/patched-vscode/build/npm/dirs.js b/patched-vscode/build/npm/dirs.js index 2aa1c43f..9f057b3a 100644 --- a/patched-vscode/build/npm/dirs.js +++ b/patched-vscode/build/npm/dirs.js @@ -40,8 +40,10 @@ const dirs = [ 'extensions/php-language-features', 'extensions/references-view', 'extensions/sagemaker-extension', + 'extensions/sagemaker-extensions-sync', 'extensions/sagemaker-idle-extension', 'extensions/sagemaker-terminal-crash-mitigation', + 'extensions/sagemaker-open-notebook-extension', 'extensions/search-result', 'extensions/simple-browser', 'extensions/tunnel-forwarding', diff --git a/patched-vscode/extensions/sagemaker-extensions-sync/.vscodeignore b/patched-vscode/extensions/sagemaker-extensions-sync/.vscodeignore new file mode 100644 index 00000000..56b78554 --- /dev/null +++ b/patched-vscode/extensions/sagemaker-extensions-sync/.vscodeignore @@ -0,0 +1,12 @@ +.vscode/** +.vscode-test/** +out/test/** +out/** +test/** +src/** +tsconfig.json +out/test/** +out/** +cgmanifest.json +yarn.lock +preview-src/** diff --git a/patched-vscode/extensions/sagemaker-extensions-sync/README.md b/patched-vscode/extensions/sagemaker-extensions-sync/README.md new file mode 100644 index 00000000..b8dd030e --- /dev/null +++ b/patched-vscode/extensions/sagemaker-extensions-sync/README.md @@ -0,0 +1,3 @@ +# SageMaker Code Editor Extensions Sync + +Notifies users if the extensions directory is missing pre-packaged extensions from SageMaker Distribution and give them the option to sync them. \ No newline at end of file diff --git a/patched-vscode/extensions/sagemaker-extensions-sync/extension-browser.webpack.config.js b/patched-vscode/extensions/sagemaker-extensions-sync/extension-browser.webpack.config.js new file mode 100644 index 00000000..68271e0e --- /dev/null +++ b/patched-vscode/extensions/sagemaker-extensions-sync/extension-browser.webpack.config.js @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright Amazon.com Inc. or its affiliates. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check + +'use strict'; + +const withBrowserDefaults = require('../shared.webpack.config').browser; + +module.exports = withBrowserDefaults({ + context: __dirname, + entry: { + extension: './src/extension.ts' + }, +}); diff --git a/patched-vscode/extensions/sagemaker-extensions-sync/extension.webpack.config.js b/patched-vscode/extensions/sagemaker-extensions-sync/extension.webpack.config.js new file mode 100644 index 00000000..59852626 --- /dev/null +++ b/patched-vscode/extensions/sagemaker-extensions-sync/extension.webpack.config.js @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright Amazon.com Inc. or its affiliates. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check + +'use strict'; + +const withDefaults = require('../shared.webpack.config'); + +module.exports = withDefaults({ + context: __dirname, + resolve: { + mainFields: ['module', 'main'] + }, + entry: { + extension: './src/extension.ts', + } +}); diff --git a/patched-vscode/extensions/sagemaker-extensions-sync/package.json b/patched-vscode/extensions/sagemaker-extensions-sync/package.json new file mode 100644 index 00000000..a0761fa0 --- /dev/null +++ b/patched-vscode/extensions/sagemaker-extensions-sync/package.json @@ -0,0 +1,44 @@ +{ + "name": "sagemaker-extensions-sync", + "displayName": "SageMaker Extensions Sync", + "description": "Sync pre-packaged extensions from SageMaker Distribution", + "extensionKind": [ + "workspace" + ], + "version": "1.0.0", + "publisher": "sagemaker", + "license": "MIT", + "engines": { + "vscode": "^1.70.0" + }, + "main": "./out/extension", + "categories": [ + "Other" + ], + "activationEvents": [ + "*" + ], + "capabilities": { + "virtualWorkspaces": true, + "untrustedWorkspaces": { + "supported": true + } + }, + "contributes": { + "commands": [ + { + "command": "extensions-sync.syncExtensions", + "title": "Sync Extensions from SageMaker Distribution", + "category": "Extensions Sync" + } + ] + }, + "scripts": { + "compile": "gulp compile-extension:sagemaker-extensions-sync", + "watch": "npm run build-preview && gulp watch-extension:sagemaker-extensions-sync", + "vscode:prepublish": "npm run build-ext", + "build-ext": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:sagemaker-idle-extension ./tsconfig.json" + }, + "dependencies": {}, + "repository": {} +} diff --git a/patched-vscode/extensions/sagemaker-extensions-sync/src/constants.ts b/patched-vscode/extensions/sagemaker-extensions-sync/src/constants.ts new file mode 100644 index 00000000..1a7fdcb8 --- /dev/null +++ b/patched-vscode/extensions/sagemaker-extensions-sync/src/constants.ts @@ -0,0 +1,21 @@ +// constants +export const PERSISTENT_VOLUME_EXTENSIONS_DIR = "/home/sagemaker-user/sagemaker-code-editor-server-data/extensions"; +export const IMAGE_EXTENSIONS_DIR = "/opt/amazon/sagemaker/sagemaker-code-editor-server-data/extensions"; +export const LOG_PREFIX = "[sagemaker-extensions-sync]"; + +export class ExtensionInfo { + constructor( + public name: string, + public publisher: string, + public version: string, + public path: string | null + ) {} + + get identifier(): string { + return `${this.publisher}.${this.name}@${this.version}`; + } + + toString(): string { + return `ExtensionInfo: ${this.identifier} (${this.path})`; + } +} diff --git a/patched-vscode/extensions/sagemaker-extensions-sync/src/extension.ts b/patched-vscode/extensions/sagemaker-extensions-sync/src/extension.ts new file mode 100644 index 00000000..f9f44fd5 --- /dev/null +++ b/patched-vscode/extensions/sagemaker-extensions-sync/src/extension.ts @@ -0,0 +1,100 @@ +import * as process from "process"; +import * as vscode from 'vscode'; + +import { + ExtensionInfo, + IMAGE_EXTENSIONS_DIR, + LOG_PREFIX, + PERSISTENT_VOLUME_EXTENSIONS_DIR, +} from "./constants" + +import { + getExtensionsFromDirectory, + getInstalledExtensions, + installExtension, + refreshExtensionsMetadata } from "./utils" + +export async function activate() { + + // this extension will only activate within a sagemaker app + const isSageMakerApp = !!process.env?.SAGEMAKER_APP_TYPE_LOWERCASE; + if (!isSageMakerApp) { + return; + } + + // get installed extensions. this could be different from pvExtensions b/c vscode sometimes doesn't delete the assets + // for an old extension when uninstalling or changing versions + const installedExtensions = new Set(await getInstalledExtensions()); + console.log(`${LOG_PREFIX} Found installed extensions: `, Array.from(installedExtensions)); + + const prePackagedExtensions: ExtensionInfo[] = await getExtensionsFromDirectory(IMAGE_EXTENSIONS_DIR); + const prePackagedExtensionsById: Record = {}; + prePackagedExtensions.forEach(extension => { + prePackagedExtensionsById[extension.identifier] = extension; + }); + + console.log(`${LOG_PREFIX} Found pre-packaged extensions: `, prePackagedExtensions); + + const pvExtensions = await getExtensionsFromDirectory(PERSISTENT_VOLUME_EXTENSIONS_DIR); + const pvExtensionsByName: Record = {}; + const pvExtensionsById: Record = {}; + pvExtensions.forEach(extension => { + if (installedExtensions.has(extension.identifier)) { // only index extensions that are installed + pvExtensionsByName[extension.name] = extension; + pvExtensionsById[extension.identifier] = extension; + } + }); + console.log(`${LOG_PREFIX} Found installed extensions in persistent volume: `, pvExtensionsById); + + // check each pre-packaged extension, record if it is not in installed extensions or version mismatch + // store unsynced extensions as {identifier pre-packaged ext: currently installed version} + const unsyncedExtensions: Record = {} + prePackagedExtensions.forEach(extension => { + const id = extension.identifier; + if (!(installedExtensions.has(id))){ + unsyncedExtensions[id] = pvExtensionsByName[extension.name]?.version ?? null; + } + }); + console.log(`${LOG_PREFIX} Unsynced extensions: `, unsyncedExtensions); + + if (Object.keys(unsyncedExtensions).length !== 0) { + const selection = await vscode.window.showWarningMessage( + 'Warning: You have unsynchronized extensions from SageMaker Distribution \ + which could result in incompatibilities with Code Editor. Do you want to install them?', + "Synchronize Extensions", "Dismiss"); + + if (selection === "Synchronize Extensions") { + const quickPick = vscode.window.createQuickPick(); + quickPick.items = Object.keys(unsyncedExtensions).map(extensionId => ({ + label: extensionId, + description: unsyncedExtensions[extensionId] ? `Currently installed version: ${unsyncedExtensions[extensionId]}` : undefined, + })); + quickPick.placeholder = 'Select extensions to install'; + quickPick.canSelectMany = true; + quickPick.ignoreFocusOut = true; + + quickPick.onDidAccept(async () => { + const selectedExtensions = quickPick.selectedItems.map(item => item.label); + + for (const extensionId of selectedExtensions) { + const extensionName = prePackagedExtensionsById[extensionId].name; + await installExtension(prePackagedExtensionsById[extensionId], pvExtensionsByName[extensionName]); + } + await refreshExtensionsMetadata(); + + quickPick.hide(); + await vscode.window.showInformationMessage( + 'Extensions have been installed. \nWould you like to reload the window?', + { modal: true }, + 'Reload' + ).then(selection => { + if (selection === 'Reload') { + vscode.commands.executeCommand('workbench.action.reloadWindow'); + } + }); + }); + + quickPick.show(); + } + } +} \ No newline at end of file diff --git a/patched-vscode/extensions/sagemaker-extensions-sync/src/utils.ts b/patched-vscode/extensions/sagemaker-extensions-sync/src/utils.ts new file mode 100644 index 00000000..e2d34fe0 --- /dev/null +++ b/patched-vscode/extensions/sagemaker-extensions-sync/src/utils.ts @@ -0,0 +1,152 @@ +import * as fs from "fs/promises"; +import * as path from "path"; +import * as vscode from 'vscode'; +import { execFile } from "child_process"; +import { promisify } from "util"; + +import { + ExtensionInfo, + LOG_PREFIX, + PERSISTENT_VOLUME_EXTENSIONS_DIR, +} from "./constants" + +export async function getExtensionsFromDirectory(directoryPath: string): Promise { + const results: ExtensionInfo[] = []; + try { + const items = await fs.readdir(directoryPath); + + for (const item of items) { + const itemPath = path.join(directoryPath, item); + try { + const stats = await fs.stat(itemPath); + + if (stats.isDirectory()) { + const packageJsonPath = path.join(itemPath, "package.json"); + + const packageData = JSON.parse(await fs.readFile(packageJsonPath, "utf8")); + + if (packageData.name && packageData.publisher && packageData.version) { + results.push(new ExtensionInfo( + packageData.name, + packageData.publisher, + packageData.version, + itemPath, + )); + } + } + } catch (error) { + // fs.stat will break on dangling simlinks. Just skip to the next file + console.error(`${LOG_PREFIX} Error reading package.json in ${itemPath}:`, error); + } + } + } catch (error) { + console.error(`${LOG_PREFIX} Error reading directory ${directoryPath}:`, error); + } + return results; +} + +export async function getInstalledExtensions(): Promise { + const command = "sagemaker-code-editor"; + const args = ["--list-extensions", "--show-versions", "--extensions-dir", PERSISTENT_VOLUME_EXTENSIONS_DIR]; + + const execFileAsync = promisify(execFile); + try { + const { stdout, stderr } = await execFileAsync(command, args); + if (stderr) { + throw new Error("stderr"); + } + return stdout.split("\n").filter(line => line.trim() !== ""); + } catch (error) { + console.error(`${LOG_PREFIX} Error getting list of installed extensions:`, error); + throw error; + } +} + +export async function refreshExtensionsMetadata(): Promise { + const metaDataFile = path.join(PERSISTENT_VOLUME_EXTENSIONS_DIR, "extensions.json"); + try { + await fs.unlink(metaDataFile); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { + console.error(`${LOG_PREFIX} Error removing metadata file:`, error); + } + } +} + +export async function installExtension( + prePackagedExtensionInfo: ExtensionInfo, installedExtensionInfo?: ExtensionInfo | undefined +): Promise { + if (installedExtensionInfo) { + console.log(`${LOG_PREFIX} Upgrading extension from ${installedExtensionInfo.identifier} to ${prePackagedExtensionInfo.identifier}`); + } else { + console.log(`${LOG_PREFIX} Installing extension ${prePackagedExtensionInfo.identifier}`); + } + try { + if (!prePackagedExtensionInfo.path) { + throw new Error(`Extension path missing for ${prePackagedExtensionInfo.identifier}`); + } + + const targetPath = path.join(PERSISTENT_VOLUME_EXTENSIONS_DIR, path.basename(prePackagedExtensionInfo.path)); + + // Remove existing symlink or directory if it exists + try { + console.log(`${LOG_PREFIX} Removing existing folder ${targetPath}`); + await fs.unlink(targetPath); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { + console.error(`${LOG_PREFIX} Error removing existing extension:`, error); + throw error; + } + // if file already doesn't exist then keep going + } + + // Create new symlink + try { + console.log(`${LOG_PREFIX} Adding extension to persistent volume directory`); + await fs.symlink(prePackagedExtensionInfo.path, targetPath, 'dir'); + } catch (error) { + console.error(`${LOG_PREFIX} Error adding extension to persistent volume directory:`, error); + throw error; + } + + // Handle .obsolete file + const OBSOLETE_FILE = path.join(PERSISTENT_VOLUME_EXTENSIONS_DIR, '.obsolete'); + let obsoleteData: Record = {}; + + try { + const obsoleteContent = await fs.readFile(OBSOLETE_FILE, 'utf-8'); + console.log(`${LOG_PREFIX} .obsolete file found`); + obsoleteData = JSON.parse(obsoleteContent); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + console.log(`${LOG_PREFIX} .obsolete file not found. Creating a new one.`); + } else { + console.warn(`${LOG_PREFIX} Error reading .obsolete file:`, error); + // Backup malformed file + const backupPath = `${OBSOLETE_FILE}.bak`; + await fs.rename(OBSOLETE_FILE, backupPath); + console.log(`${LOG_PREFIX} Backed up malformed .obsolete file to ${backupPath}`); + } + } + + if (installedExtensionInfo?.path) { + const obsoleteBasename = path.basename(installedExtensionInfo.path); + obsoleteData[obsoleteBasename] = true; + } + const obsoleteBasenamePrepackaged = path.basename(prePackagedExtensionInfo.path); + obsoleteData[obsoleteBasenamePrepackaged] = false; + + try { + console.log(`${LOG_PREFIX} Writing to .obsolete file.`); + await fs.writeFile(OBSOLETE_FILE, JSON.stringify(obsoleteData, null, 2)); + } catch (error) { + console.error(`${LOG_PREFIX} Error writing .obsolete file:`, error); + throw error; + } + + console.log(`${LOG_PREFIX} Installed ${prePackagedExtensionInfo.identifier}`); + } catch (error) { + vscode.window.showErrorMessage(`Could not install extension ${prePackagedExtensionInfo.identifier}`); + console.error(`${LOG_PREFIX} ${error}`); + } +} \ No newline at end of file diff --git a/patched-vscode/extensions/sagemaker-extensions-sync/tsconfig.json b/patched-vscode/extensions/sagemaker-extensions-sync/tsconfig.json new file mode 100644 index 00000000..e474d9a5 --- /dev/null +++ b/patched-vscode/extensions/sagemaker-extensions-sync/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./out" + }, + "include": [ + "../sagemaker-extensions-sync/src/**/*", + "../../src/vscode-dts/vscode.d.ts" + ] +} diff --git a/patched-vscode/extensions/sagemaker-extensions-sync/yarn.lock b/patched-vscode/extensions/sagemaker-extensions-sync/yarn.lock new file mode 100644 index 00000000..fb57ccd1 --- /dev/null +++ b/patched-vscode/extensions/sagemaker-extensions-sync/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/patched-vscode/extensions/sagemaker-idle-extension/src/extension.ts b/patched-vscode/extensions/sagemaker-idle-extension/src/extension.ts index 0fbb35e6..b6c1f46a 100644 --- a/patched-vscode/extensions/sagemaker-idle-extension/src/extension.ts +++ b/patched-vscode/extensions/sagemaker-idle-extension/src/extension.ts @@ -21,20 +21,17 @@ export function deactivate() { /** * Initializes the file path where the idle timestamp will be stored. - * It sets the path to a hidden file in the user's home directory. + * It sets the path to a hidden file in the /tmp/ directory. */ function initializeIdleFilePath() { - const homeDirectory = process.env.HOME || process.env.USERPROFILE; - if (!homeDirectory) { - console.log(`${LOG_PREFIX} Unable to determine the home directory.`); - return; - } - idleFilePath = path.join(homeDirectory, ".sagemaker-last-active-timestamp"); + const tmpDirectory = "/tmp/"; + idleFilePath = path.join(tmpDirectory, ".sagemaker-last-active-timestamp"); // Set initial lastActivetimestamp updateLastActivityTimestamp() } + /** * Registers event listeners to monitor user activity within the VSCode editor. * It listens to document changes, editor focus changes, text selection changes, and terminal events. diff --git a/patched-vscode/extensions/sagemaker-open-notebook-extension/.vscodeignore b/patched-vscode/extensions/sagemaker-open-notebook-extension/.vscodeignore new file mode 100644 index 00000000..56b78554 --- /dev/null +++ b/patched-vscode/extensions/sagemaker-open-notebook-extension/.vscodeignore @@ -0,0 +1,12 @@ +.vscode/** +.vscode-test/** +out/test/** +out/** +test/** +src/** +tsconfig.json +out/test/** +out/** +cgmanifest.json +yarn.lock +preview-src/** diff --git a/patched-vscode/extensions/sagemaker-open-notebook-extension/README.md b/patched-vscode/extensions/sagemaker-open-notebook-extension/README.md new file mode 100644 index 00000000..0efc94ac --- /dev/null +++ b/patched-vscode/extensions/sagemaker-open-notebook-extension/README.md @@ -0,0 +1,18 @@ +# Code Editor Open Notebook Extension + +The Open Notebook extension enables users to download, transform, and display sample notebooks from a public Amazon S3 bucket owned by the SageMaker team. This extension streamlines the process of accessing and working with SageMaker sample notebooks directly within Code Editor. + +## Features + +- Download sample notebooks from a specified S3 bucket +- Transform notebooks for compatibility with VSCode +- Display notebooks within the Code Editor environment +- Utilize URL parameters to open specific notebooks + +## Usage + +The extension uses parameters from the URL to open the desired notebook. The required parameters are: +- Notebook key: The identifier for the specific notebook in the S3 bucket +- Cluster ID: The ID of the SageMaker cluster +- Region: The AWS region where the S3 bucket is located + diff --git a/patched-vscode/extensions/sagemaker-open-notebook-extension/extension-browser.webpack.config.js b/patched-vscode/extensions/sagemaker-open-notebook-extension/extension-browser.webpack.config.js new file mode 100644 index 00000000..68271e0e --- /dev/null +++ b/patched-vscode/extensions/sagemaker-open-notebook-extension/extension-browser.webpack.config.js @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright Amazon.com Inc. or its affiliates. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check + +'use strict'; + +const withBrowserDefaults = require('../shared.webpack.config').browser; + +module.exports = withBrowserDefaults({ + context: __dirname, + entry: { + extension: './src/extension.ts' + }, +}); diff --git a/patched-vscode/extensions/sagemaker-open-notebook-extension/extension.webpack.config.js b/patched-vscode/extensions/sagemaker-open-notebook-extension/extension.webpack.config.js new file mode 100644 index 00000000..59852626 --- /dev/null +++ b/patched-vscode/extensions/sagemaker-open-notebook-extension/extension.webpack.config.js @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright Amazon.com Inc. or its affiliates. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check + +'use strict'; + +const withDefaults = require('../shared.webpack.config'); + +module.exports = withDefaults({ + context: __dirname, + resolve: { + mainFields: ['module', 'main'] + }, + entry: { + extension: './src/extension.ts', + } +}); diff --git a/patched-vscode/extensions/sagemaker-open-notebook-extension/package.json b/patched-vscode/extensions/sagemaker-open-notebook-extension/package.json new file mode 100644 index 00000000..6a62df9c --- /dev/null +++ b/patched-vscode/extensions/sagemaker-open-notebook-extension/package.json @@ -0,0 +1,44 @@ +{ + "name": "sagemaker-open-notebook-extension", + "displayName": "Sagemaker open notebook Extension", + "description": "To download and open sample notebook when open code editor", + "extensionKind": [ + "workspace" + ], + "version": "1.0.0", + "publisher": "sagemaker", + "license": "MIT", + "engines": { + "vscode": "^1.70.0" + }, + "main": "./out/extension", + "categories": [ + "Other" + ], + "activationEvents": [ + "*" + ], + "capabilities": { + "virtualWorkspaces": true, + "untrustedWorkspaces": { + "supported": true + } + }, + "contributes": { + "configuration": { + "type": "object", + "title": "Sagemaker Open Notebook Extension", + "properties": {} + }, + "commands": [ + ] + }, + "scripts": { + "compile": "gulp compile-extension:sagemaker-open-notebook-extension", + "watch": "npm run build-preview && gulp watch-extension:sagemaker-open-notebook-extension", + "vscode:prepublish": "npm run build-ext", + "build-ext": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:sagemaker-open-notebook-extension ./tsconfig.json" + }, + "dependencies": {}, + "repository": {} +} diff --git a/patched-vscode/extensions/sagemaker-open-notebook-extension/src/extension.ts b/patched-vscode/extensions/sagemaker-open-notebook-extension/src/extension.ts new file mode 100644 index 00000000..4e81577a --- /dev/null +++ b/patched-vscode/extensions/sagemaker-open-notebook-extension/src/extension.ts @@ -0,0 +1,91 @@ + +import * as vscode from 'vscode'; +import * as https from 'https'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import * as console from 'console'; + +export function activate() { + const config = vscode.workspace.getConfiguration('extensions.openNotebookData'); + const notebookKey = config.get('notebookKey') as string; + const clusterId = config.get('clusterId') as string; + const region = config.get('region') as string; + if(notebookKey){ + loadAndDisplayNotebook(notebookKey, clusterId, region); + } + +} + +function isValidRegion(region: string): boolean { + // This regex allows for characters, numbers, and hyphens + const regionRegex = /^[a-zA-Z0-9-]+$/; + return regionRegex.test(region); +} + +async function loadAndDisplayNotebook(fileKey: string, clusterId: string, region: string) { + if (!isValidRegion(region)) { + vscode.window.showErrorMessage('Invalid region format. Region should only contain characters, numbers, and hyphens.'); + return; + } + + const bucketName = `jumpstart-cache-prod-${region}`; + const url = `https://${bucketName}.s3.${region}.amazonaws.com/${fileKey}`; + try { + let content = await downloadFile(url); + content = processNotebookContent(content, clusterId, region); + const tempDir = os.tmpdir(); + const tempFilePath = path.join(tempDir, 'downloaded-notebook.ipynb'); + fs.writeFileSync(tempFilePath, content); + const uri = vscode.Uri.file(tempFilePath); + await openNotebookDocument(uri); + } catch (error) { + vscode.window.showErrorMessage('Error downloading or opening notebook: ' + error.message); + } +} + +function processNotebookContent(content: string, clusterId: string, region: string): string { + const notebook = JSON.parse(content); + notebook.cells = notebook.cells.map((cell: any) => { + if (cell.metadata && + cell.metadata.jumpStartAlterations && + cell.metadata.jumpStartAlterations.includes('clusterId')) { + cell.source = [ + "%%bash\n", + `aws ssm start-session --target sagemaker-cluster:${clusterId} --region ${region}` + ]; + cell.cell_type = "code"; + } + return cell; + }); + return JSON.stringify(notebook, null, 2); +} + +async function openNotebookDocument(uri: vscode.Uri) { + try { + // Open the notebook document + const document = await vscode.workspace.openNotebookDocument(uri); + // Show the notebook document in a notebook editor + await vscode.window.showNotebookDocument(document); + } catch (error) { + console.error('Failed to open notebook:', error); + vscode.window.showErrorMessage('Failed to open notebook: ' + error.message); + } +} + +function downloadFile(url: string): Promise { + return new Promise((resolve, reject) => { + https.get(url, (response) => { + let data = ''; + response.on('data', (chunk) => { + data += chunk; + }); + response.on('end', () => { + resolve(data); + }); + }).on('error', (error) => { + reject(error); + }); + }); +} +export function deactivate() {} diff --git a/patched-vscode/extensions/sagemaker-open-notebook-extension/tsconfig.json b/patched-vscode/extensions/sagemaker-open-notebook-extension/tsconfig.json new file mode 100644 index 00000000..3e1b4164 --- /dev/null +++ b/patched-vscode/extensions/sagemaker-open-notebook-extension/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./out" + }, + "include": [ + "../sagemaker-open-notebook-extension/src/**/*", + "../../src/vscode-dts/vscode.d.ts" + ] +} diff --git a/patched-vscode/extensions/sagemaker-open-notebook-extension/yarn.lock b/patched-vscode/extensions/sagemaker-open-notebook-extension/yarn.lock new file mode 100644 index 00000000..fb57ccd1 --- /dev/null +++ b/patched-vscode/extensions/sagemaker-open-notebook-extension/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/patched-vscode/src/vs/base/common/platform.ts b/patched-vscode/src/vs/base/common/platform.ts index 2251c7db..aac70df5 100644 --- a/patched-vscode/src/vs/base/common/platform.ts +++ b/patched-vscode/src/vs/base/common/platform.ts @@ -2,8 +2,6 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; - export const LANGUAGE_DEFAULT = 'en'; let _isWindows = false; @@ -112,17 +110,21 @@ else if (typeof navigator === 'object' && !isElectronRenderer) { _isMobile = _userAgent?.indexOf('Mobi') >= 0; _isWeb = true; - const configuredLocale = nls.getConfiguredDefaultLocale( - // This call _must_ be done in the file that calls `nls.getConfiguredDefaultLocale` - // to ensure that the NLS AMD Loader plugin has been loaded and configured. - // This is because the loader plugin decides what the default locale is based on - // how it's able to resolve the strings. - nls.localize({ key: 'ensureLoaderPluginIsLoaded', comment: ['{Locked}'] }, '_') - ); - - _locale = configuredLocale || LANGUAGE_DEFAULT; + _locale = LANGUAGE_DEFAULT; _language = _locale; _platformLocale = navigator.language; + const el = typeof document !== 'undefined' && document.getElementById('vscode-remote-nls-configuration'); + const rawNlsConfig = el && el.getAttribute('data-settings'); + if (rawNlsConfig) { + try { + const nlsConfig: NLSConfig = JSON.parse(rawNlsConfig); + const resolved = nlsConfig.availableLanguages['*']; + _locale = nlsConfig.locale; + _platformLocale = nlsConfig.osLocale; + _language = resolved ? resolved : LANGUAGE_DEFAULT; + _translationsConfigFile = nlsConfig._translationsConfigFile; + } catch (error) { /* Oh well. */ } + } } // Unknown environment diff --git a/patched-vscode/src/vs/code/browser/workbench/workbench.html b/patched-vscode/src/vs/code/browser/workbench/workbench.html index cfee57cc..96fe4a0b 100644 --- a/patched-vscode/src/vs/code/browser/workbench/workbench.html +++ b/patched-vscode/src/vs/code/browser/workbench/workbench.html @@ -19,6 +19,9 @@ + + + @@ -46,14 +49,26 @@ // Normalize locale to lowercase because translationServiceUrl is case-sensitive. // ref: https://github.com/microsoft/vscode/issues/187795 const locale = localStorage.getItem('vscode.nls.locale') || navigator.language.toLowerCase(); - if (!locale.startsWith('en')) { - nlsConfig['vs/nls'] = { - availableLanguages: { - '*': locale - }, - translationServiceUrl: '{{WORKBENCH_NLS_BASE_URL}}' - }; - } + try { + nlsConfig['vs/nls'] = JSON.parse(document.getElementById("vscode-remote-nls-configuration").getAttribute("data-settings")) + if (nlsConfig['vs/nls']._resolvedLanguagePackCoreLocation) { + const bundles = Object.create(null) + nlsConfig['vs/nls'].loadBundle = (bundle, _language, cb) => { + const result = bundles[bundle] + if (result) { + return cb(undefined, result) + } + const path = nlsConfig['vs/nls']._resolvedLanguagePackCoreLocation + "/" + bundle.replace(/\//g, "!") + ".nls.json" + fetch(`{{WORKBENCH_WEB_BASE_URL}}/../vscode-remote-resource?path=${encodeURIComponent(path)}`) + .then((response) => response.json()) + .then((json) => { + bundles[bundle] = json + cb(undefined, json) + }) + .catch(cb) + } + } + } catch (error) { /* Probably fine. */ } require.config({ baseUrl: `${baseUrl}/out`, diff --git a/patched-vscode/src/vs/platform/environment/common/environmentService.ts b/patched-vscode/src/vs/platform/environment/common/environmentService.ts index cd55aa9b..a09dde3f 100644 --- a/patched-vscode/src/vs/platform/environment/common/environmentService.ts +++ b/patched-vscode/src/vs/platform/environment/common/environmentService.ts @@ -101,7 +101,7 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron return URI.file(join(vscodePortable, 'argv.json')); } - return joinPath(this.userHome, this.productService.dataFolderName, 'argv.json'); + return joinPath(this.appSettingsHome, 'argv.json'); } @memoize diff --git a/patched-vscode/src/vs/platform/languagePacks/browser/languagePacks.ts b/patched-vscode/src/vs/platform/languagePacks/browser/languagePacks.ts index 62dedb22..c83c335b 100644 --- a/patched-vscode/src/vs/platform/languagePacks/browser/languagePacks.ts +++ b/patched-vscode/src/vs/platform/languagePacks/browser/languagePacks.ts @@ -5,18 +5,24 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { URI } from 'vs/base/common/uri'; +import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionResourceLoaderService } from 'vs/platform/extensionResourceLoader/common/extensionResourceLoader'; -import { ILanguagePackItem, LanguagePackBaseService } from 'vs/platform/languagePacks/common/languagePacks'; +import { ILanguagePackItem, ILanguagePackService, LanguagePackBaseService } from 'vs/platform/languagePacks/common/languagePacks'; import { ILogService } from 'vs/platform/log/common/log'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; export class WebLanguagePacksService extends LanguagePackBaseService { + private readonly languagePackService: ILanguagePackService; + constructor( + @IRemoteAgentService remoteAgentService: IRemoteAgentService, @IExtensionResourceLoaderService private readonly extensionResourceLoaderService: IExtensionResourceLoaderService, @IExtensionGalleryService extensionGalleryService: IExtensionGalleryService, @ILogService private readonly logService: ILogService ) { super(extensionGalleryService); + this.languagePackService = ProxyChannel.toService(remoteAgentService.getConnection()!.getChannel('languagePacks')) } async getBuiltInExtensionTranslationsUri(id: string, language: string): Promise { @@ -72,6 +78,6 @@ export class WebLanguagePacksService extends LanguagePackBaseService { // Web doesn't have a concept of language packs, so we just return an empty array getInstalledLanguages(): Promise { - return Promise.resolve([]); + return this.languagePackService.getInstalledLanguages() } } diff --git a/patched-vscode/src/vs/server/node/remoteLanguagePacks.ts b/patched-vscode/src/vs/server/node/remoteLanguagePacks.ts index 682b6f2b..2b39c3f8 100644 --- a/patched-vscode/src/vs/server/node/remoteLanguagePacks.ts +++ b/patched-vscode/src/vs/server/node/remoteLanguagePacks.ts @@ -8,36 +8,30 @@ import { FileAccess } from 'vs/base/common/network'; import * as path from 'vs/base/common/path'; import * as lp from 'vs/base/node/languagePacks'; -import product from 'vs/platform/product/common/product'; const metaData = path.join(FileAccess.asFileUri('').fsPath, 'nls.metadata.json'); const _cache: Map> = new Map(); -function exists(file: string) { - return new Promise(c => fs.exists(file, c)); -} - export function getNLSConfiguration(language: string, userDataPath: string): Promise { - return exists(metaData).then((fileExists) => { - if (!fileExists || !product.commit) { - // console.log(`==> MetaData or commit unknown. Using default language.`); - // The OS Locale on the remote side really doesn't matter, so we return the default locale - return Promise.resolve({ locale: 'en', osLocale: 'en', availableLanguages: {} }); - } - const key = `${language}||${userDataPath}`; - let result = _cache.get(key); - if (!result) { - // The OS Locale on the remote side really doesn't matter, so we pass in the same language - result = lp.getNLSConfiguration(product.commit, userDataPath, metaData, language, language).then(value => { - if (InternalNLSConfiguration.is(value)) { - value._languagePackSupport = true; - } - return value; - }); - _cache.set(key, result); - } - return result; - }); + const key = `${language}||${userDataPath}`; + let result = _cache.get(key); + if (!result) { + // The OS Locale on the remote side really doesn't matter, so we pass in the same language + result = lp.getNLSConfiguration("dummy_commmit", userDataPath, metaData, language, language).then(value => { + if (InternalNLSConfiguration.is(value)) { + value._languagePackSupport = true; + } + // If the configuration has no results keep trying since code-server + // doesn't restart when a language is installed so this result would + // persist (the plugin might not be installed yet for example). + if (value.locale !== 'en' && value.locale !== 'en-us' && Object.keys(value.availableLanguages).length === 0) { + _cache.delete(key); + } + return value; + }); + _cache.set(key, result); + } + return result; } export namespace InternalNLSConfiguration { @@ -46,3 +40,43 @@ export namespace InternalNLSConfiguration { return candidate && typeof candidate._languagePackId === 'string'; } } + +/** + * The code below is copied from from src/main.js. + */ + +export const getLocaleFromConfig = async (argvResource: string): Promise => { + try { + const content = stripComments(await fs.promises.readFile(argvResource, 'utf8')); + return JSON.parse(content).locale; + } catch (error) { + if (error.code !== "ENOENT") { + console.warn(error) + } + return 'en'; + } +}; + +const stripComments = (content: string): string => { + const regexp = /('(?:[^\\']*(?:\\.)?)*')|('(?:[^\\']*(?:\\.)?)*')|(\/\*(?:\r?\n|.)*?\*\/)|(\/{2,}.*?(?:(?:\r?\n)|$))/g; + + return content.replace(regexp, (match, _m1, _m2, m3, m4) => { + // Only one of m1, m2, m3, m4 matches + if (m3) { + // A block comment. Replace with nothing + return ''; + } else if (m4) { + // A line comment. If it ends in \r?\n then keep it. + const length_1 = m4.length; + if (length_1 > 2 && m4[length_1 - 1] === '\n') { + return m4[length_1 - 2] === '\r' ? '\r\n' : '\n'; + } + else { + return ''; + } + } else { + // We match a string + return match; + } + }); +}; diff --git a/patched-vscode/src/vs/server/node/serverEnvironmentService.ts b/patched-vscode/src/vs/server/node/serverEnvironmentService.ts index 83d4aac7..0bbddc94 100644 --- a/patched-vscode/src/vs/server/node/serverEnvironmentService.ts +++ b/patched-vscode/src/vs/server/node/serverEnvironmentService.ts @@ -14,6 +14,8 @@ import { URI } from 'vs/base/common/uri'; export const serverOptions: OptionDescriptions> = { + 'locale': { type: 'string' }, + /* ----- server setup ----- */ 'host': { type: 'string', cat: 'o', args: 'ip-address', description: nls.localize('host', "The host name or IP address the server should listen to. If not set, defaults to 'localhost'.") }, @@ -97,6 +99,8 @@ export const serverOptions: OptionDescriptions> = { export interface ServerParsedArgs { + 'locale'?: string; + /* ----- server setup ----- */ host?: string; diff --git a/patched-vscode/src/vs/server/node/serverServices.ts b/patched-vscode/src/vs/server/node/serverServices.ts index 46f2f004..e3609574 100644 --- a/patched-vscode/src/vs/server/node/serverServices.ts +++ b/patched-vscode/src/vs/server/node/serverServices.ts @@ -11,7 +11,7 @@ import * as path from 'vs/base/common/path'; import { IURITransformer } from 'vs/base/common/uriIpc'; import { getMachineId, getSqmMachineId, getdevDeviceId } from 'vs/base/node/id'; import { Promises } from 'vs/base/node/pfs'; -import { ClientConnectionEvent, IMessagePassingProtocol, IPCServer, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; +import { ClientConnectionEvent, IMessagePassingProtocol, IPCServer, ProxyChannel, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; import { ProtocolConstants } from 'vs/base/parts/ipc/common/ipc.net'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; @@ -225,6 +225,9 @@ export async function setupServerServices(connectionToken: ServerConnectionToken const channel = new ExtensionManagementChannel(extensionManagementService, (ctx: RemoteAgentConnectionContext) => getUriTransformer(ctx.remoteAuthority)); socketServer.registerChannel('extensions', channel); + const languagePackChannel = ProxyChannel.fromService(accessor.get(ILanguagePackService), disposables); + socketServer.registerChannel('languagePacks', languagePackChannel); + // clean up extensions folder remoteExtensionsScanner.whenExtensionsReady().then(() => extensionManagementService.cleanUp()); diff --git a/patched-vscode/src/vs/server/node/webClientServer.ts b/patched-vscode/src/vs/server/node/webClientServer.ts index 670d3bf0..5646dab9 100644 --- a/patched-vscode/src/vs/server/node/webClientServer.ts +++ b/patched-vscode/src/vs/server/node/webClientServer.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createReadStream } from 'fs'; +import { createReadStream, existsSync, writeFileSync } from 'fs'; import {readFile } from 'fs/promises'; import { Promises } from 'vs/base/node/pfs'; import * as path from 'path'; @@ -28,6 +28,7 @@ import { URI } from 'vs/base/common/uri'; import { streamToBuffer } from 'vs/base/common/buffer'; import { IProductConfiguration } from 'vs/base/common/product'; import { isString } from 'vs/base/common/types'; +import { getLocaleFromConfig, getNLSConfiguration } from 'vs/server/node/remoteLanguagePacks'; import { CharCode } from 'vs/base/common/charCode'; import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; @@ -351,6 +352,8 @@ export class WebClientServer { callbackRoute: this._callbackRoute }; + const locale = this._environmentService.args.locale || await getLocaleFromConfig(this._environmentService.argvResource.fsPath); + const nlsConfiguration = await getNLSConfiguration(locale, this._environmentService.userDataPath) const nlsBaseUrl = this._productService.extensionsGallery?.nlsBaseUrl; const values: { [key: string]: string } = { WORKBENCH_WEB_CONFIGURATION: asJSON(workbenchWebConfiguration), @@ -359,6 +362,7 @@ export class WebClientServer { WORKBENCH_NLS_BASE_URL: vscodeBase + (nlsBaseUrl ? `${nlsBaseUrl}${!nlsBaseUrl.endsWith('/') ? '/' : ''}${this._productService.commit}/${this._productService.version}/` : ''), BASE: base, VS_BASE: vscodeBase, + NLS_CONFIGURATION: asJSON(nlsConfiguration), }; if (useTestResolver) { @@ -390,7 +394,7 @@ export class WebClientServer { `frame-src 'self' https://*.vscode-cdn.net data:;`, 'worker-src \'self\' data: blob:;', 'style-src \'self\' \'unsafe-inline\';', - 'connect-src \'self\' ws: wss: https:;', + 'connect-src \'self\' ws: wss: https://main.vscode-cdn.net http://localhost:* https://localhost:* https://login.microsoftonline.com/ https://update.code.visualstudio.com https://*.vscode-unpkg.net/ https://default.exp-tas.com/vscode/ab https://vscode-sync.trafficmanager.net https://vscode-sync-insiders.trafficmanager.net https://*.gallerycdn.vsassets.io https://marketplace.visualstudio.com https://*.blob.core.windows.net https://az764295.vo.msecnd.net https://code.visualstudio.com https://*.gallery.vsassets.io https://*.rel.tunnels.api.visualstudio.com wss://*.rel.tunnels.api.visualstudio.com https://*.servicebus.windows.net/ https://vscode.blob.core.windows.net https://vscode.search.windows.net https://vsmarketplacebadges.dev https://vscode.download.prss.microsoft.com https://download.visualstudio.microsoft.com https://*.vscode-unpkg.net https://open-vsx.org;', 'font-src \'self\' blob:;', 'manifest-src \'self\';' ].join(' '); @@ -463,12 +467,16 @@ export class WebClientServer { */ private async _handleIdle(req: http.IncomingMessage, res: http.ServerResponse): Promise { try { - const homeDirectory = process.env.HOME || process.env.USERPROFILE; - if (!homeDirectory) { - throw new Error('Home directory not found'); + const tmpDirectory = '/tmp/' + const idleFilePath = path.join(tmpDirectory, '.sagemaker-last-active-timestamp'); + + // If idle shutdown file does not exist, this indicates the app UI may never been opened + // Create the initial metadata file + if (!existsSync(idleFilePath)) { + const timestamp = new Date().toISOString(); + writeFileSync(idleFilePath, timestamp); } - const idleFilePath = path.join(homeDirectory, '.code-editor-last-active-timestamp'); const data = await readFile(idleFilePath, 'utf8'); res.statusCode = 200; diff --git a/patched-vscode/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/patched-vscode/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index 0ff4dca2..43dc8077 100644 --- a/patched-vscode/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/patched-vscode/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -258,7 +258,12 @@ Registry.as(ConfigurationExtensions.Configuration) type: 'boolean', description: localize('extensionsInQuickAccess', "When enabled, extensions can be searched for via Quick Access and report issues from there."), default: true - } + }, + 'extensions.openNotebookData': { + type: 'object', + scope: ConfigurationScope.APPLICATION, + default: {}, + }, } }); diff --git a/patched-vscode/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/patched-vscode/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index fc174208..e5e2bfd0 100644 --- a/patched-vscode/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/patched-vscode/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -341,9 +341,6 @@ export class InstallAction extends ExtensionAction { if (this.extension.isBuiltin) { return; } - if (this.extensionsWorkbenchService.canSetLanguage(this.extension)) { - return; - } if (this.extension.state === ExtensionState.Uninstalled && await this.extensionsWorkbenchService.canInstall(this.extension)) { this.enabled = this.options.installPreReleaseVersion ? this.extension.hasPreReleaseVersion : this.extension.hasReleaseVersion; this.updateLabel(); @@ -614,7 +611,7 @@ export abstract class InstallInOtherServerAction extends ExtensionAction { } if (isLanguagePackExtension(this.extension.local.manifest)) { - return true; + return false; } // Prefers to run on UI @@ -1848,17 +1845,6 @@ export class SetLanguageAction extends ExtensionAction { update(): void { this.enabled = false; this.class = SetLanguageAction.DisabledClass; - if (!this.extension) { - return; - } - if (!this.extensionsWorkbenchService.canSetLanguage(this.extension)) { - return; - } - if (this.extension.gallery && language === getLocale(this.extension.gallery)) { - return; - } - this.enabled = true; - this.class = SetLanguageAction.EnabledClass; } override async run(): Promise { @@ -1875,7 +1861,6 @@ export class ClearLanguageAction extends ExtensionAction { private static readonly DisabledClass = `${ClearLanguageAction.EnabledClass} disabled`; constructor( - @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @ILocaleService private readonly localeService: ILocaleService, ) { super(ClearLanguageAction.ID, ClearLanguageAction.TITLE.value, ClearLanguageAction.DisabledClass, false); @@ -1885,17 +1870,6 @@ export class ClearLanguageAction extends ExtensionAction { update(): void { this.enabled = false; this.class = ClearLanguageAction.DisabledClass; - if (!this.extension) { - return; - } - if (!this.extensionsWorkbenchService.canSetLanguage(this.extension)) { - return; - } - if (this.extension.gallery && language !== getLocale(this.extension.gallery)) { - return; - } - this.enabled = true; - this.class = ClearLanguageAction.EnabledClass; } override async run(): Promise { diff --git a/patched-vscode/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/patched-vscode/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 3eb8c7ee..83cb13bb 100644 --- a/patched-vscode/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/patched-vscode/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -950,6 +950,17 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension urlService.registerHandler(this); this.whenInitialized = this.initialize(); + + //Update workspace to open notebook + const urlParams = new URLSearchParams(window.location.search); + const notebookKey = urlParams.get('openNotebook'); + const clusterId = urlParams.get('clusterId'); + const region = urlParams.get('region'); + configurationService.updateValue('extensions.openNotebookData', { + notebookKey: notebookKey, + clusterId: clusterId, + region: region, + }); } private async initialize(): Promise { diff --git a/patched-vscode/src/vs/workbench/services/localization/electron-sandbox/localeService.ts b/patched-vscode/src/vs/workbench/services/localization/electron-sandbox/localeService.ts index d7124758..4a7686df 100644 --- a/patched-vscode/src/vs/workbench/services/localization/electron-sandbox/localeService.ts +++ b/patched-vscode/src/vs/workbench/services/localization/electron-sandbox/localeService.ts @@ -51,7 +51,8 @@ class NativeLocaleService implements ILocaleService { @IProductService private readonly productService: IProductService ) { } - private async validateLocaleFile(): Promise { + // Make public just so we do not have to patch all the unused code out. + public async validateLocaleFile(): Promise { try { const content = await this.textFileService.read(this.environmentService.argvResource, { encoding: 'utf8' }); @@ -78,9 +79,6 @@ class NativeLocaleService implements ILocaleService { } private async writeLocaleValue(locale: string | undefined): Promise { - if (!(await this.validateLocaleFile())) { - return false; - } await this.jsonEditingService.write(this.environmentService.argvResource, [{ path: ['locale'], value: locale }], true); return true; } diff --git a/patched-vscode/src/vs/workbench/workbench.web.main.ts b/patched-vscode/src/vs/workbench/workbench.web.main.ts index e4a0a7e3..bdbc65d6 100644 --- a/patched-vscode/src/vs/workbench/workbench.web.main.ts +++ b/patched-vscode/src/vs/workbench/workbench.web.main.ts @@ -52,7 +52,7 @@ import 'vs/workbench/services/dialogs/browser/fileDialogService'; import 'vs/workbench/services/host/browser/browserHostService'; import 'vs/workbench/services/lifecycle/browser/lifecycleService'; import 'vs/workbench/services/clipboard/browser/clipboardService'; -import 'vs/workbench/services/localization/browser/localeService'; +import 'vs/workbench/services/localization/electron-sandbox/localeService'; import 'vs/workbench/services/path/browser/pathService'; import 'vs/workbench/services/themes/browser/browserHostColorSchemeService'; import 'vs/workbench/services/encryption/browser/encryptionService'; @@ -118,8 +118,9 @@ registerSingleton(ILanguagePackService, WebLanguagePacksService, InstantiationTy // Logs import 'vs/workbench/contrib/logs/browser/logs.contribution'; -// Localization -import 'vs/workbench/contrib/localization/browser/localization.contribution'; +// Localization. This does not actually import anything specific to Electron so +// it should be safe. +import 'vs/workbench/contrib/localization/electron-sandbox/localization.contribution'; // Performance import 'vs/workbench/contrib/performance/browser/performance.web.contribution'; diff --git a/patches/display-language.diff b/patches/display-language.diff new file mode 100644 index 00000000..d8d279db --- /dev/null +++ b/patches/display-language.diff @@ -0,0 +1,457 @@ +Add display language support + +We can remove this once upstream supports all language packs. + +1. Proxies language packs to the service on the backend. +2. NLS configuration is embedded into the HTML for the browser to pick up. This + code to generate this configuration is copied from the native portion. +3. Remove configuredLocale since we have our own thing. +4. Move the argv.json file to the server instead of in-browser storage. This is + where the current locale is stored and currently the server needs to be able + to read it. +5. Add the locale flag. +6. Remove the redundant locale verification. It does the same as the existing + one but is worse because it does not handle non-existent or empty files. +7. Replace some caching and Node requires because code-server does not restart + when changing the language unlike native Code. +8. Make language extensions installable like normal rather than using the + special set/clear language actions. + +Index: sagemaker-code-editor/vscode/src/vs/platform/languagePacks/browser/languagePacks.ts +=================================================================== +--- sagemaker-code-editor.orig/vscode/src/vs/platform/languagePacks/browser/languagePacks.ts ++++ sagemaker-code-editor/vscode/src/vs/platform/languagePacks/browser/languagePacks.ts +@@ -5,18 +5,24 @@ + + import { CancellationTokenSource } from 'vs/base/common/cancellation'; + import { URI } from 'vs/base/common/uri'; ++import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; + import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; + import { IExtensionResourceLoaderService } from 'vs/platform/extensionResourceLoader/common/extensionResourceLoader'; +-import { ILanguagePackItem, LanguagePackBaseService } from 'vs/platform/languagePacks/common/languagePacks'; ++import { ILanguagePackItem, ILanguagePackService, LanguagePackBaseService } from 'vs/platform/languagePacks/common/languagePacks'; + import { ILogService } from 'vs/platform/log/common/log'; ++import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; + + export class WebLanguagePacksService extends LanguagePackBaseService { ++ private readonly languagePackService: ILanguagePackService; ++ + constructor( ++ @IRemoteAgentService remoteAgentService: IRemoteAgentService, + @IExtensionResourceLoaderService private readonly extensionResourceLoaderService: IExtensionResourceLoaderService, + @IExtensionGalleryService extensionGalleryService: IExtensionGalleryService, + @ILogService private readonly logService: ILogService + ) { + super(extensionGalleryService); ++ this.languagePackService = ProxyChannel.toService(remoteAgentService.getConnection()!.getChannel('languagePacks')) + } + + async getBuiltInExtensionTranslationsUri(id: string, language: string): Promise { +@@ -72,6 +78,6 @@ export class WebLanguagePacksService ext + + // Web doesn't have a concept of language packs, so we just return an empty array + getInstalledLanguages(): Promise { +- return Promise.resolve([]); ++ return this.languagePackService.getInstalledLanguages() + } + } +Index: sagemaker-code-editor/vscode/src/vs/server/node/serverServices.ts +=================================================================== +--- sagemaker-code-editor.orig/vscode/src/vs/server/node/serverServices.ts ++++ sagemaker-code-editor/vscode/src/vs/server/node/serverServices.ts +@@ -11,7 +11,7 @@ import * as path from 'vs/base/common/pa + import { IURITransformer } from 'vs/base/common/uriIpc'; + import { getMachineId, getSqmMachineId, getdevDeviceId } from 'vs/base/node/id'; + import { Promises } from 'vs/base/node/pfs'; +-import { ClientConnectionEvent, IMessagePassingProtocol, IPCServer, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; ++import { ClientConnectionEvent, IMessagePassingProtocol, IPCServer, ProxyChannel, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; + import { ProtocolConstants } from 'vs/base/parts/ipc/common/ipc.net'; + import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; + import { ConfigurationService } from 'vs/platform/configuration/common/configurationService'; +@@ -225,6 +225,9 @@ export async function setupServerService + const channel = new ExtensionManagementChannel(extensionManagementService, (ctx: RemoteAgentConnectionContext) => getUriTransformer(ctx.remoteAuthority)); + socketServer.registerChannel('extensions', channel); + ++ const languagePackChannel = ProxyChannel.fromService(accessor.get(ILanguagePackService), disposables); ++ socketServer.registerChannel('languagePacks', languagePackChannel); ++ + // clean up extensions folder + remoteExtensionsScanner.whenExtensionsReady().then(() => extensionManagementService.cleanUp()); + +Index: sagemaker-code-editor/vscode/src/vs/base/common/platform.ts +=================================================================== +--- sagemaker-code-editor.orig/vscode/src/vs/base/common/platform.ts ++++ sagemaker-code-editor/vscode/src/vs/base/common/platform.ts +@@ -2,8 +2,6 @@ + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +-import * as nls from 'vs/nls'; +- + export const LANGUAGE_DEFAULT = 'en'; + + let _isWindows = false; +@@ -112,17 +110,21 @@ else if (typeof navigator === 'object' & + _isMobile = _userAgent?.indexOf('Mobi') >= 0; + _isWeb = true; + +- const configuredLocale = nls.getConfiguredDefaultLocale( +- // This call _must_ be done in the file that calls `nls.getConfiguredDefaultLocale` +- // to ensure that the NLS AMD Loader plugin has been loaded and configured. +- // This is because the loader plugin decides what the default locale is based on +- // how it's able to resolve the strings. +- nls.localize({ key: 'ensureLoaderPluginIsLoaded', comment: ['{Locked}'] }, '_') +- ); +- +- _locale = configuredLocale || LANGUAGE_DEFAULT; ++ _locale = LANGUAGE_DEFAULT; + _language = _locale; + _platformLocale = navigator.language; ++ const el = typeof document !== 'undefined' && document.getElementById('vscode-remote-nls-configuration'); ++ const rawNlsConfig = el && el.getAttribute('data-settings'); ++ if (rawNlsConfig) { ++ try { ++ const nlsConfig: NLSConfig = JSON.parse(rawNlsConfig); ++ const resolved = nlsConfig.availableLanguages['*']; ++ _locale = nlsConfig.locale; ++ _platformLocale = nlsConfig.osLocale; ++ _language = resolved ? resolved : LANGUAGE_DEFAULT; ++ _translationsConfigFile = nlsConfig._translationsConfigFile; ++ } catch (error) { /* Oh well. */ } ++ } + } + + // Unknown environment +Index: sagemaker-code-editor/vscode/src/vs/code/browser/workbench/workbench.html +=================================================================== +--- sagemaker-code-editor.orig/vscode/src/vs/code/browser/workbench/workbench.html ++++ sagemaker-code-editor/vscode/src/vs/code/browser/workbench/workbench.html +@@ -19,6 +19,9 @@ + + + ++ ++ ++ + + + +@@ -46,14 +49,26 @@ + // Normalize locale to lowercase because translationServiceUrl is case-sensitive. + // ref: https://github.com/microsoft/vscode/issues/187795 + const locale = localStorage.getItem('vscode.nls.locale') || navigator.language.toLowerCase(); +- if (!locale.startsWith('en')) { +- nlsConfig['vs/nls'] = { +- availableLanguages: { +- '*': locale +- }, +- translationServiceUrl: '{{WORKBENCH_NLS_BASE_URL}}' +- }; +- } ++ try { ++ nlsConfig['vs/nls'] = JSON.parse(document.getElementById("vscode-remote-nls-configuration").getAttribute("data-settings")) ++ if (nlsConfig['vs/nls']._resolvedLanguagePackCoreLocation) { ++ const bundles = Object.create(null) ++ nlsConfig['vs/nls'].loadBundle = (bundle, _language, cb) => { ++ const result = bundles[bundle] ++ if (result) { ++ return cb(undefined, result) ++ } ++ const path = nlsConfig['vs/nls']._resolvedLanguagePackCoreLocation + "/" + bundle.replace(/\//g, "!") + ".nls.json" ++ fetch(`{{WORKBENCH_WEB_BASE_URL}}/../vscode-remote-resource?path=${encodeURIComponent(path)}`) ++ .then((response) => response.json()) ++ .then((json) => { ++ bundles[bundle] = json ++ cb(undefined, json) ++ }) ++ .catch(cb) ++ } ++ } ++ } catch (error) { /* Probably fine. */ } + + require.config({ + baseUrl: `${baseUrl}/out`, +Index: sagemaker-code-editor/vscode/src/vs/platform/environment/common/environmentService.ts +=================================================================== +--- sagemaker-code-editor.orig/vscode/src/vs/platform/environment/common/environmentService.ts ++++ sagemaker-code-editor/vscode/src/vs/platform/environment/common/environmentService.ts +@@ -101,7 +101,7 @@ export abstract class AbstractNativeEnvi + return URI.file(join(vscodePortable, 'argv.json')); + } + +- return joinPath(this.userHome, this.productService.dataFolderName, 'argv.json'); ++ return joinPath(this.appSettingsHome, 'argv.json'); + } + + @memoize +Index: sagemaker-code-editor/vscode/src/vs/server/node/remoteLanguagePacks.ts +=================================================================== +--- sagemaker-code-editor.orig/vscode/src/vs/server/node/remoteLanguagePacks.ts ++++ sagemaker-code-editor/vscode/src/vs/server/node/remoteLanguagePacks.ts +@@ -8,36 +8,30 @@ import { FileAccess } from 'vs/base/comm + import * as path from 'vs/base/common/path'; + + import * as lp from 'vs/base/node/languagePacks'; +-import product from 'vs/platform/product/common/product'; + + const metaData = path.join(FileAccess.asFileUri('').fsPath, 'nls.metadata.json'); + const _cache: Map> = new Map(); + +-function exists(file: string) { +- return new Promise(c => fs.exists(file, c)); +-} +- + export function getNLSConfiguration(language: string, userDataPath: string): Promise { +- return exists(metaData).then((fileExists) => { +- if (!fileExists || !product.commit) { +- // console.log(`==> MetaData or commit unknown. Using default language.`); +- // The OS Locale on the remote side really doesn't matter, so we return the default locale +- return Promise.resolve({ locale: 'en', osLocale: 'en', availableLanguages: {} }); +- } +- const key = `${language}||${userDataPath}`; +- let result = _cache.get(key); +- if (!result) { +- // The OS Locale on the remote side really doesn't matter, so we pass in the same language +- result = lp.getNLSConfiguration(product.commit, userDataPath, metaData, language, language).then(value => { +- if (InternalNLSConfiguration.is(value)) { +- value._languagePackSupport = true; +- } +- return value; +- }); +- _cache.set(key, result); +- } +- return result; +- }); ++ const key = `${language}||${userDataPath}`; ++ let result = _cache.get(key); ++ if (!result) { ++ // The OS Locale on the remote side really doesn't matter, so we pass in the same language ++ result = lp.getNLSConfiguration("dummy_commmit", userDataPath, metaData, language, language).then(value => { ++ if (InternalNLSConfiguration.is(value)) { ++ value._languagePackSupport = true; ++ } ++ // If the configuration has no results keep trying since code-server ++ // doesn't restart when a language is installed so this result would ++ // persist (the plugin might not be installed yet for example). ++ if (value.locale !== 'en' && value.locale !== 'en-us' && Object.keys(value.availableLanguages).length === 0) { ++ _cache.delete(key); ++ } ++ return value; ++ }); ++ _cache.set(key, result); ++ } ++ return result; + } + + export namespace InternalNLSConfiguration { +@@ -46,3 +40,43 @@ export namespace InternalNLSConfiguratio + return candidate && typeof candidate._languagePackId === 'string'; + } + } ++ ++/** ++ * The code below is copied from from src/main.js. ++ */ ++ ++export const getLocaleFromConfig = async (argvResource: string): Promise => { ++ try { ++ const content = stripComments(await fs.promises.readFile(argvResource, 'utf8')); ++ return JSON.parse(content).locale; ++ } catch (error) { ++ if (error.code !== "ENOENT") { ++ console.warn(error) ++ } ++ return 'en'; ++ } ++}; ++ ++const stripComments = (content: string): string => { ++ const regexp = /('(?:[^\\']*(?:\\.)?)*')|('(?:[^\\']*(?:\\.)?)*')|(\/\*(?:\r?\n|.)*?\*\/)|(\/{2,}.*?(?:(?:\r?\n)|$))/g; ++ ++ return content.replace(regexp, (match, _m1, _m2, m3, m4) => { ++ // Only one of m1, m2, m3, m4 matches ++ if (m3) { ++ // A block comment. Replace with nothing ++ return ''; ++ } else if (m4) { ++ // A line comment. If it ends in \r?\n then keep it. ++ const length_1 = m4.length; ++ if (length_1 > 2 && m4[length_1 - 1] === '\n') { ++ return m4[length_1 - 2] === '\r' ? '\r\n' : '\n'; ++ } ++ else { ++ return ''; ++ } ++ } else { ++ // We match a string ++ return match; ++ } ++ }); ++}; +Index: sagemaker-code-editor/vscode/src/vs/server/node/webClientServer.ts +=================================================================== +--- sagemaker-code-editor.orig/vscode/src/vs/server/node/webClientServer.ts ++++ sagemaker-code-editor/vscode/src/vs/server/node/webClientServer.ts +@@ -28,6 +28,7 @@ import { URI } from 'vs/base/common/uri' + import { streamToBuffer } from 'vs/base/common/buffer'; + import { IProductConfiguration } from 'vs/base/common/product'; + import { isString } from 'vs/base/common/types'; ++import { getLocaleFromConfig, getNLSConfiguration } from 'vs/server/node/remoteLanguagePacks'; + import { CharCode } from 'vs/base/common/charCode'; + import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; + +@@ -351,6 +352,8 @@ export class WebClientServer { + callbackRoute: this._callbackRoute + }; + ++ const locale = this._environmentService.args.locale || await getLocaleFromConfig(this._environmentService.argvResource.fsPath); ++ const nlsConfiguration = await getNLSConfiguration(locale, this._environmentService.userDataPath) + const nlsBaseUrl = this._productService.extensionsGallery?.nlsBaseUrl; + const values: { [key: string]: string } = { + WORKBENCH_WEB_CONFIGURATION: asJSON(workbenchWebConfiguration), +@@ -359,6 +362,7 @@ export class WebClientServer { + WORKBENCH_NLS_BASE_URL: vscodeBase + (nlsBaseUrl ? `${nlsBaseUrl}${!nlsBaseUrl.endsWith('/') ? '/' : ''}${this._productService.commit}/${this._productService.version}/` : ''), + BASE: base, + VS_BASE: vscodeBase, ++ NLS_CONFIGURATION: asJSON(nlsConfiguration), + }; + + if (useTestResolver) { +Index: sagemaker-code-editor/vscode/src/vs/server/node/serverEnvironmentService.ts +=================================================================== +--- sagemaker-code-editor.orig/vscode/src/vs/server/node/serverEnvironmentService.ts ++++ sagemaker-code-editor/vscode/src/vs/server/node/serverEnvironmentService.ts +@@ -14,6 +14,8 @@ import { URI } from 'vs/base/common/uri' + + export const serverOptions: OptionDescriptions> = { + ++ 'locale': { type: 'string' }, ++ + /* ----- server setup ----- */ + + 'host': { type: 'string', cat: 'o', args: 'ip-address', description: nls.localize('host', "The host name or IP address the server should listen to. If not set, defaults to 'localhost'.") }, +@@ -97,6 +99,8 @@ export const serverOptions: OptionDescri + + export interface ServerParsedArgs { + ++ 'locale'?: string; ++ + /* ----- server setup ----- */ + + host?: string; +Index: sagemaker-code-editor/vscode/src/vs/workbench/workbench.web.main.ts +=================================================================== +--- sagemaker-code-editor.orig/vscode/src/vs/workbench/workbench.web.main.ts ++++ sagemaker-code-editor/vscode/src/vs/workbench/workbench.web.main.ts +@@ -52,7 +52,7 @@ import 'vs/workbench/services/dialogs/br + import 'vs/workbench/services/host/browser/browserHostService'; + import 'vs/workbench/services/lifecycle/browser/lifecycleService'; + import 'vs/workbench/services/clipboard/browser/clipboardService'; +-import 'vs/workbench/services/localization/browser/localeService'; ++import 'vs/workbench/services/localization/electron-sandbox/localeService'; + import 'vs/workbench/services/path/browser/pathService'; + import 'vs/workbench/services/themes/browser/browserHostColorSchemeService'; + import 'vs/workbench/services/encryption/browser/encryptionService'; +@@ -118,8 +118,9 @@ registerSingleton(ILanguagePackService, + // Logs + import 'vs/workbench/contrib/logs/browser/logs.contribution'; + +-// Localization +-import 'vs/workbench/contrib/localization/browser/localization.contribution'; ++// Localization. This does not actually import anything specific to Electron so ++// it should be safe. ++import 'vs/workbench/contrib/localization/electron-sandbox/localization.contribution'; + + // Performance + import 'vs/workbench/contrib/performance/browser/performance.web.contribution'; +Index: sagemaker-code-editor/vscode/src/vs/workbench/services/localization/electron-sandbox/localeService.ts +=================================================================== +--- sagemaker-code-editor.orig/vscode/src/vs/workbench/services/localization/electron-sandbox/localeService.ts ++++ sagemaker-code-editor/vscode/src/vs/workbench/services/localization/electron-sandbox/localeService.ts +@@ -51,7 +51,8 @@ class NativeLocaleService implements ILo + @IProductService private readonly productService: IProductService + ) { } + +- private async validateLocaleFile(): Promise { ++ // Make public just so we do not have to patch all the unused code out. ++ public async validateLocaleFile(): Promise { + try { + const content = await this.textFileService.read(this.environmentService.argvResource, { encoding: 'utf8' }); + +@@ -78,9 +79,6 @@ class NativeLocaleService implements ILo + } + + private async writeLocaleValue(locale: string | undefined): Promise { +- if (!(await this.validateLocaleFile())) { +- return false; +- } + await this.jsonEditingService.write(this.environmentService.argvResource, [{ path: ['locale'], value: locale }], true); + return true; + } +Index: sagemaker-code-editor/vscode/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +=================================================================== +--- sagemaker-code-editor.orig/vscode/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts ++++ sagemaker-code-editor/vscode/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +@@ -341,9 +341,6 @@ export class InstallAction extends Exten + if (this.extension.isBuiltin) { + return; + } +- if (this.extensionsWorkbenchService.canSetLanguage(this.extension)) { +- return; +- } + if (this.extension.state === ExtensionState.Uninstalled && await this.extensionsWorkbenchService.canInstall(this.extension)) { + this.enabled = this.options.installPreReleaseVersion ? this.extension.hasPreReleaseVersion : this.extension.hasReleaseVersion; + this.updateLabel(); +@@ -614,7 +611,7 @@ export abstract class InstallInOtherServ + } + + if (isLanguagePackExtension(this.extension.local.manifest)) { +- return true; ++ return false; + } + + // Prefers to run on UI +@@ -1848,17 +1845,6 @@ export class SetLanguageAction extends E + update(): void { + this.enabled = false; + this.class = SetLanguageAction.DisabledClass; +- if (!this.extension) { +- return; +- } +- if (!this.extensionsWorkbenchService.canSetLanguage(this.extension)) { +- return; +- } +- if (this.extension.gallery && language === getLocale(this.extension.gallery)) { +- return; +- } +- this.enabled = true; +- this.class = SetLanguageAction.EnabledClass; + } + + override async run(): Promise { +@@ -1875,7 +1861,6 @@ export class ClearLanguageAction extends + private static readonly DisabledClass = `${ClearLanguageAction.EnabledClass} disabled`; + + constructor( +- @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @ILocaleService private readonly localeService: ILocaleService, + ) { + super(ClearLanguageAction.ID, ClearLanguageAction.TITLE.value, ClearLanguageAction.DisabledClass, false); +@@ -1885,17 +1870,6 @@ export class ClearLanguageAction extends + update(): void { + this.enabled = false; + this.class = ClearLanguageAction.DisabledClass; +- if (!this.extension) { +- return; +- } +- if (!this.extensionsWorkbenchService.canSetLanguage(this.extension)) { +- return; +- } +- if (this.extension.gallery && language !== getLocale(this.extension.gallery)) { +- return; +- } +- this.enabled = true; +- this.class = ClearLanguageAction.EnabledClass; + } + + override async run(): Promise { diff --git a/patches/sagemaker-extensions-sync.patch b/patches/sagemaker-extensions-sync.patch new file mode 100644 index 00000000..2d6e6315 --- /dev/null +++ b/patches/sagemaker-extensions-sync.patch @@ -0,0 +1,460 @@ +Index: sagemaker-code-editor/vscode/build/gulpfile.extensions.js +=================================================================== +--- sagemaker-code-editor.orig/vscode/build/gulpfile.extensions.js ++++ sagemaker-code-editor/vscode/build/gulpfile.extensions.js +@@ -62,6 +62,7 @@ const compilations = [ + 'extensions/simple-browser/tsconfig.json', + 'extensions/sagemaker-extension/tsconfig.json', + 'extensions/sagemaker-idle-extension/tsconfig.json', ++ 'extensions/sagemaker-extensions-sync/tsconfig.json', + 'extensions/sagemaker-terminal-crash-mitigation/tsconfig.json', + 'extensions/sagemaker-open-notebook-extension/tsconfig.json', + 'extensions/tunnel-forwarding/tsconfig.json', +Index: sagemaker-code-editor/vscode/build/npm/dirs.js +=================================================================== +--- sagemaker-code-editor.orig/vscode/build/npm/dirs.js ++++ sagemaker-code-editor/vscode/build/npm/dirs.js +@@ -40,6 +40,7 @@ const dirs = [ + 'extensions/php-language-features', + 'extensions/references-view', + 'extensions/sagemaker-extension', ++ 'extensions/sagemaker-extensions-sync', + 'extensions/sagemaker-idle-extension', + 'extensions/sagemaker-terminal-crash-mitigation', + 'extensions/sagemaker-open-notebook-extension', +Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/.vscodeignore +=================================================================== +--- /dev/null ++++ sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/.vscodeignore +@@ -0,0 +1,12 @@ ++.vscode/** ++.vscode-test/** ++out/test/** ++out/** ++test/** ++src/** ++tsconfig.json ++out/test/** ++out/** ++cgmanifest.json ++yarn.lock ++preview-src/** +Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/README.md +=================================================================== +--- /dev/null ++++ sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/README.md +@@ -0,0 +1,3 @@ ++# SageMaker Code Editor Extensions Sync ++ ++Notifies users if the extensions directory is missing pre-packaged extensions from SageMaker Distribution and give them the option to sync them. +\ No newline at end of file +Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/extension-browser.webpack.config.js +=================================================================== +--- /dev/null ++++ sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/extension-browser.webpack.config.js +@@ -0,0 +1,17 @@ ++/*--------------------------------------------------------------------------------------------- ++ * Copyright Amazon.com Inc. or its affiliates. All rights reserved. ++ * Licensed under the MIT License. See License.txt in the project root for license information. ++ *--------------------------------------------------------------------------------------------*/ ++ ++//@ts-check ++ ++'use strict'; ++ ++const withBrowserDefaults = require('../shared.webpack.config').browser; ++ ++module.exports = withBrowserDefaults({ ++ context: __dirname, ++ entry: { ++ extension: './src/extension.ts' ++ }, ++}); +Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/extension.webpack.config.js +=================================================================== +--- /dev/null ++++ sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/extension.webpack.config.js +@@ -0,0 +1,20 @@ ++/*--------------------------------------------------------------------------------------------- ++ * Copyright Amazon.com Inc. or its affiliates. All rights reserved. ++ * Licensed under the MIT License. See License.txt in the project root for license information. ++ *--------------------------------------------------------------------------------------------*/ ++ ++//@ts-check ++ ++'use strict'; ++ ++const withDefaults = require('../shared.webpack.config'); ++ ++module.exports = withDefaults({ ++ context: __dirname, ++ resolve: { ++ mainFields: ['module', 'main'] ++ }, ++ entry: { ++ extension: './src/extension.ts', ++ } ++}); +Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/package.json +=================================================================== +--- /dev/null ++++ sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/package.json +@@ -0,0 +1,44 @@ ++{ ++ "name": "sagemaker-extensions-sync", ++ "displayName": "SageMaker Extensions Sync", ++ "description": "Sync pre-packaged extensions from SageMaker Distribution", ++ "extensionKind": [ ++ "workspace" ++ ], ++ "version": "1.0.0", ++ "publisher": "sagemaker", ++ "license": "MIT", ++ "engines": { ++ "vscode": "^1.70.0" ++ }, ++ "main": "./out/extension", ++ "categories": [ ++ "Other" ++ ], ++ "activationEvents": [ ++ "*" ++ ], ++ "capabilities": { ++ "virtualWorkspaces": true, ++ "untrustedWorkspaces": { ++ "supported": true ++ } ++ }, ++ "contributes": { ++ "commands": [ ++ { ++ "command": "extensions-sync.syncExtensions", ++ "title": "Sync Extensions from SageMaker Distribution", ++ "category": "Extensions Sync" ++ } ++ ] ++ }, ++ "scripts": { ++ "compile": "gulp compile-extension:sagemaker-extensions-sync", ++ "watch": "npm run build-preview && gulp watch-extension:sagemaker-extensions-sync", ++ "vscode:prepublish": "npm run build-ext", ++ "build-ext": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:sagemaker-idle-extension ./tsconfig.json" ++ }, ++ "dependencies": {}, ++ "repository": {} ++} +Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/constants.ts +=================================================================== +--- /dev/null ++++ sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/constants.ts +@@ -0,0 +1,21 @@ ++// constants ++export const PERSISTENT_VOLUME_EXTENSIONS_DIR = "/home/sagemaker-user/sagemaker-code-editor-server-data/extensions"; ++export const IMAGE_EXTENSIONS_DIR = "/opt/amazon/sagemaker/sagemaker-code-editor-server-data/extensions"; ++export const LOG_PREFIX = "[sagemaker-extensions-sync]"; ++ ++export class ExtensionInfo { ++ constructor( ++ public name: string, ++ public publisher: string, ++ public version: string, ++ public path: string | null ++ ) {} ++ ++ get identifier(): string { ++ return `${this.publisher}.${this.name}@${this.version}`; ++ } ++ ++ toString(): string { ++ return `ExtensionInfo: ${this.identifier} (${this.path})`; ++ } ++} +Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/extension.ts +=================================================================== +--- /dev/null ++++ sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/extension.ts +@@ -0,0 +1,100 @@ ++import * as process from "process"; ++import * as vscode from 'vscode'; ++ ++import { ++ ExtensionInfo, ++ IMAGE_EXTENSIONS_DIR, ++ LOG_PREFIX, ++ PERSISTENT_VOLUME_EXTENSIONS_DIR, ++} from "./constants" ++ ++import { ++ getExtensionsFromDirectory, ++ getInstalledExtensions, ++ installExtension, ++ refreshExtensionsMetadata } from "./utils" ++ ++export async function activate() { ++ ++ // this extension will only activate within a sagemaker app ++ const isSageMakerApp = !!process.env?.SAGEMAKER_APP_TYPE_LOWERCASE; ++ if (!isSageMakerApp) { ++ return; ++ } ++ ++ // get installed extensions. this could be different from pvExtensions b/c vscode sometimes doesn't delete the assets ++ // for an old extension when uninstalling or changing versions ++ const installedExtensions = new Set(await getInstalledExtensions()); ++ console.log(`${LOG_PREFIX} Found installed extensions: `, Array.from(installedExtensions)); ++ ++ const prePackagedExtensions: ExtensionInfo[] = await getExtensionsFromDirectory(IMAGE_EXTENSIONS_DIR); ++ const prePackagedExtensionsById: Record = {}; ++ prePackagedExtensions.forEach(extension => { ++ prePackagedExtensionsById[extension.identifier] = extension; ++ }); ++ ++ console.log(`${LOG_PREFIX} Found pre-packaged extensions: `, prePackagedExtensions); ++ ++ const pvExtensions = await getExtensionsFromDirectory(PERSISTENT_VOLUME_EXTENSIONS_DIR); ++ const pvExtensionsByName: Record = {}; ++ const pvExtensionsById: Record = {}; ++ pvExtensions.forEach(extension => { ++ if (installedExtensions.has(extension.identifier)) { // only index extensions that are installed ++ pvExtensionsByName[extension.name] = extension; ++ pvExtensionsById[extension.identifier] = extension; ++ } ++ }); ++ console.log(`${LOG_PREFIX} Found installed extensions in persistent volume: `, pvExtensionsById); ++ ++ // check each pre-packaged extension, record if it is not in installed extensions or version mismatch ++ // store unsynced extensions as {identifier pre-packaged ext: currently installed version} ++ const unsyncedExtensions: Record = {} ++ prePackagedExtensions.forEach(extension => { ++ const id = extension.identifier; ++ if (!(installedExtensions.has(id))){ ++ unsyncedExtensions[id] = pvExtensionsByName[extension.name]?.version ?? null; ++ } ++ }); ++ console.log(`${LOG_PREFIX} Unsynced extensions: `, unsyncedExtensions); ++ ++ if (Object.keys(unsyncedExtensions).length !== 0) { ++ const selection = await vscode.window.showWarningMessage( ++ 'Warning: You have unsynchronized extensions from SageMaker Distribution \ ++ which could result in incompatibilities with Code Editor. Do you want to install them?', ++ "Synchronize Extensions", "Dismiss"); ++ ++ if (selection === "Synchronize Extensions") { ++ const quickPick = vscode.window.createQuickPick(); ++ quickPick.items = Object.keys(unsyncedExtensions).map(extensionId => ({ ++ label: extensionId, ++ description: unsyncedExtensions[extensionId] ? `Currently installed version: ${unsyncedExtensions[extensionId]}` : undefined, ++ })); ++ quickPick.placeholder = 'Select extensions to install'; ++ quickPick.canSelectMany = true; ++ quickPick.ignoreFocusOut = true; ++ ++ quickPick.onDidAccept(async () => { ++ const selectedExtensions = quickPick.selectedItems.map(item => item.label); ++ ++ for (const extensionId of selectedExtensions) { ++ const extensionName = prePackagedExtensionsById[extensionId].name; ++ await installExtension(prePackagedExtensionsById[extensionId], pvExtensionsByName[extensionName]); ++ } ++ await refreshExtensionsMetadata(); ++ ++ quickPick.hide(); ++ await vscode.window.showInformationMessage( ++ 'Extensions have been installed. \nWould you like to reload the window?', ++ { modal: true }, ++ 'Reload' ++ ).then(selection => { ++ if (selection === 'Reload') { ++ vscode.commands.executeCommand('workbench.action.reloadWindow'); ++ } ++ }); ++ }); ++ ++ quickPick.show(); ++ } ++ } ++} +\ No newline at end of file +Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/tsconfig.json +=================================================================== +--- /dev/null ++++ sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/tsconfig.json +@@ -0,0 +1,10 @@ ++{ ++ "extends": "../tsconfig.base.json", ++ "compilerOptions": { ++ "outDir": "./out" ++ }, ++ "include": [ ++ "../sagemaker-extensions-sync/src/**/*", ++ "../../src/vscode-dts/vscode.d.ts" ++ ] ++} +Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/yarn.lock +=================================================================== +--- /dev/null ++++ sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/yarn.lock +@@ -0,0 +1,4 @@ ++# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. ++# yarn lockfile v1 ++ ++ +Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/utils.ts +=================================================================== +--- /dev/null ++++ sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/utils.ts +@@ -0,0 +1,152 @@ ++import * as fs from "fs/promises"; ++import * as path from "path"; ++import * as vscode from 'vscode'; ++import { execFile } from "child_process"; ++import { promisify } from "util"; ++ ++import { ++ ExtensionInfo, ++ LOG_PREFIX, ++ PERSISTENT_VOLUME_EXTENSIONS_DIR, ++} from "./constants" ++ ++export async function getExtensionsFromDirectory(directoryPath: string): Promise { ++ const results: ExtensionInfo[] = []; ++ try { ++ const items = await fs.readdir(directoryPath); ++ ++ for (const item of items) { ++ const itemPath = path.join(directoryPath, item); ++ try { ++ const stats = await fs.stat(itemPath); ++ ++ if (stats.isDirectory()) { ++ const packageJsonPath = path.join(itemPath, "package.json"); ++ ++ const packageData = JSON.parse(await fs.readFile(packageJsonPath, "utf8")); ++ ++ if (packageData.name && packageData.publisher && packageData.version) { ++ results.push(new ExtensionInfo( ++ packageData.name, ++ packageData.publisher, ++ packageData.version, ++ itemPath, ++ )); ++ } ++ } ++ } catch (error) { ++ // fs.stat will break on dangling simlinks. Just skip to the next file ++ console.error(`${LOG_PREFIX} Error reading package.json in ${itemPath}:`, error); ++ } ++ } ++ } catch (error) { ++ console.error(`${LOG_PREFIX} Error reading directory ${directoryPath}:`, error); ++ } ++ return results; ++} ++ ++export async function getInstalledExtensions(): Promise { ++ const command = "sagemaker-code-editor"; ++ const args = ["--list-extensions", "--show-versions", "--extensions-dir", PERSISTENT_VOLUME_EXTENSIONS_DIR]; ++ ++ const execFileAsync = promisify(execFile); ++ try { ++ const { stdout, stderr } = await execFileAsync(command, args); ++ if (stderr) { ++ throw new Error("stderr"); ++ } ++ return stdout.split("\n").filter(line => line.trim() !== ""); ++ } catch (error) { ++ console.error(`${LOG_PREFIX} Error getting list of installed extensions:`, error); ++ throw error; ++ } ++} ++ ++export async function refreshExtensionsMetadata(): Promise { ++ const metaDataFile = path.join(PERSISTENT_VOLUME_EXTENSIONS_DIR, "extensions.json"); ++ try { ++ await fs.unlink(metaDataFile); ++ } catch (error) { ++ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { ++ console.error(`${LOG_PREFIX} Error removing metadata file:`, error); ++ } ++ } ++} ++ ++export async function installExtension( ++ prePackagedExtensionInfo: ExtensionInfo, installedExtensionInfo?: ExtensionInfo | undefined ++): Promise { ++ if (installedExtensionInfo) { ++ console.log(`${LOG_PREFIX} Upgrading extension from ${installedExtensionInfo.identifier} to ${prePackagedExtensionInfo.identifier}`); ++ } else { ++ console.log(`${LOG_PREFIX} Installing extension ${prePackagedExtensionInfo.identifier}`); ++ } ++ try { ++ if (!prePackagedExtensionInfo.path) { ++ throw new Error(`Extension path missing for ${prePackagedExtensionInfo.identifier}`); ++ } ++ ++ const targetPath = path.join(PERSISTENT_VOLUME_EXTENSIONS_DIR, path.basename(prePackagedExtensionInfo.path)); ++ ++ // Remove existing symlink or directory if it exists ++ try { ++ console.log(`${LOG_PREFIX} Removing existing folder ${targetPath}`); ++ await fs.unlink(targetPath); ++ } catch (error) { ++ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { ++ console.error(`${LOG_PREFIX} Error removing existing extension:`, error); ++ throw error; ++ } ++ // if file already doesn't exist then keep going ++ } ++ ++ // Create new symlink ++ try { ++ console.log(`${LOG_PREFIX} Adding extension to persistent volume directory`); ++ await fs.symlink(prePackagedExtensionInfo.path, targetPath, 'dir'); ++ } catch (error) { ++ console.error(`${LOG_PREFIX} Error adding extension to persistent volume directory:`, error); ++ throw error; ++ } ++ ++ // Handle .obsolete file ++ const OBSOLETE_FILE = path.join(PERSISTENT_VOLUME_EXTENSIONS_DIR, '.obsolete'); ++ let obsoleteData: Record = {}; ++ ++ try { ++ const obsoleteContent = await fs.readFile(OBSOLETE_FILE, 'utf-8'); ++ console.log(`${LOG_PREFIX} .obsolete file found`); ++ obsoleteData = JSON.parse(obsoleteContent); ++ } catch (error) { ++ if ((error as NodeJS.ErrnoException).code === 'ENOENT') { ++ console.log(`${LOG_PREFIX} .obsolete file not found. Creating a new one.`); ++ } else { ++ console.warn(`${LOG_PREFIX} Error reading .obsolete file:`, error); ++ // Backup malformed file ++ const backupPath = `${OBSOLETE_FILE}.bak`; ++ await fs.rename(OBSOLETE_FILE, backupPath); ++ console.log(`${LOG_PREFIX} Backed up malformed .obsolete file to ${backupPath}`); ++ } ++ } ++ ++ if (installedExtensionInfo?.path) { ++ const obsoleteBasename = path.basename(installedExtensionInfo.path); ++ obsoleteData[obsoleteBasename] = true; ++ } ++ const obsoleteBasenamePrepackaged = path.basename(prePackagedExtensionInfo.path); ++ obsoleteData[obsoleteBasenamePrepackaged] = false; ++ ++ try { ++ console.log(`${LOG_PREFIX} Writing to .obsolete file.`); ++ await fs.writeFile(OBSOLETE_FILE, JSON.stringify(obsoleteData, null, 2)); ++ } catch (error) { ++ console.error(`${LOG_PREFIX} Error writing .obsolete file:`, error); ++ throw error; ++ } ++ ++ console.log(`${LOG_PREFIX} Installed ${prePackagedExtensionInfo.identifier}`); ++ } catch (error) { ++ vscode.window.showErrorMessage(`Could not install extension ${prePackagedExtensionInfo.identifier}`); ++ console.error(`${LOG_PREFIX} ${error}`); ++ } ++} +\ No newline at end of file diff --git a/patches/sagemaker-idle-extension.patch b/patches/sagemaker-idle-extension.patch index f42b600d..4dd98f63 100644 --- a/patches/sagemaker-idle-extension.patch +++ b/patches/sagemaker-idle-extension.patch @@ -147,7 +147,7 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-idle-extension/src/exte =================================================================== --- /dev/null +++ sagemaker-code-editor/vscode/extensions/sagemaker-idle-extension/src/extension.ts -@@ -0,0 +1,116 @@ +@@ -0,0 +1,112 @@ +import * as vscode from "vscode"; +import * as fs from "fs"; +import * as path from "path"; @@ -171,15 +171,11 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-idle-extension/src/exte + +/** + * Initializes the file path where the idle timestamp will be stored. -+ * It sets the path to a hidden file in the user's home directory. ++ * It sets the path to a hidden file in the /tmp/ directory. + */ +function initializeIdleFilePath() { -+ const homeDirectory = process.env.HOME || process.env.USERPROFILE; -+ if (!homeDirectory) { -+ console.log(`${LOG_PREFIX} Unable to determine the home directory.`); -+ return; -+ } -+ idleFilePath = path.join(homeDirectory, ".sagemaker-last-active-timestamp"); ++ const tmpDirectory = "/tmp/"; ++ idleFilePath = path.join(tmpDirectory, ".sagemaker-last-active-timestamp"); + + // Set initial lastActivetimestamp + updateLastActivityTimestamp() @@ -293,10 +289,12 @@ Index: sagemaker-code-editor/vscode/src/vs/server/node/webClientServer.ts =================================================================== --- sagemaker-code-editor.orig/vscode/src/vs/server/node/webClientServer.ts +++ sagemaker-code-editor/vscode/src/vs/server/node/webClientServer.ts -@@ -4,6 +4,7 @@ +@@ -3,7 +3,8 @@ + * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - import { createReadStream } from 'fs'; +-import { createReadStream } from 'fs'; ++import { createReadStream, existsSync, writeFileSync } from 'fs'; +import {readFile } from 'fs/promises'; import { Promises } from 'vs/base/node/pfs'; import * as path from 'path'; @@ -327,7 +325,7 @@ Index: sagemaker-code-editor/vscode/src/vs/server/node/webClientServer.ts if (pathname === this._callbackRoute) { // callback support return this._handleCallback(res); -@@ -451,6 +457,27 @@ export class WebClientServer { +@@ -451,6 +457,31 @@ export class WebClientServer { }); return void res.end(data); } @@ -337,12 +335,16 @@ Index: sagemaker-code-editor/vscode/src/vs/server/node/webClientServer.ts + */ + private async _handleIdle(req: http.IncomingMessage, res: http.ServerResponse): Promise { + try { -+ const homeDirectory = process.env.HOME || process.env.USERPROFILE; -+ if (!homeDirectory) { -+ throw new Error('Home directory not found'); ++ const tmpDirectory = '/tmp/' ++ const idleFilePath = path.join(tmpDirectory, '.sagemaker-last-active-timestamp'); ++ ++ // If idle shutdown file does not exist, this indicates the app UI may never been opened ++ // Create the initial metadata file ++ if (!existsSync(idleFilePath)) { ++ const timestamp = new Date().toISOString(); ++ writeFileSync(idleFilePath, timestamp); + } + -+ const idleFilePath = path.join(homeDirectory, '.sagemaker-last-active-timestamp'); + const data = await readFile(idleFilePath, 'utf8'); + + res.statusCode = 200; diff --git a/patches/sagemaker-open-notebook-extension.patch b/patches/sagemaker-open-notebook-extension.patch new file mode 100644 index 00000000..5e44c585 --- /dev/null +++ b/patches/sagemaker-open-notebook-extension.patch @@ -0,0 +1,320 @@ +Index: sagemaker-code-editor/vscode/build/gulpfile.extensions.js +=================================================================== +--- sagemaker-code-editor.orig/vscode/build/gulpfile.extensions.js ++++ sagemaker-code-editor/vscode/build/gulpfile.extensions.js +@@ -63,6 +63,7 @@ const compilations = [ + 'extensions/sagemaker-extension/tsconfig.json', + 'extensions/sagemaker-idle-extension/tsconfig.json', + 'extensions/sagemaker-terminal-crash-mitigation/tsconfig.json', ++ 'extensions/sagemaker-open-notebook-extension/tsconfig.json', + 'extensions/tunnel-forwarding/tsconfig.json', + 'extensions/typescript-language-features/test-workspace/tsconfig.json', + 'extensions/typescript-language-features/web/tsconfig.json', +Index: sagemaker-code-editor/vscode/build/npm/dirs.js +=================================================================== +--- sagemaker-code-editor.orig/vscode/build/npm/dirs.js ++++ sagemaker-code-editor/vscode/build/npm/dirs.js +@@ -42,6 +42,7 @@ const dirs = [ + 'extensions/sagemaker-extension', + 'extensions/sagemaker-idle-extension', + 'extensions/sagemaker-terminal-crash-mitigation', ++ 'extensions/sagemaker-open-notebook-extension', + 'extensions/search-result', + 'extensions/simple-browser', + 'extensions/tunnel-forwarding', +Index: sagemaker-code-editor/vscode/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +=================================================================== +--- sagemaker-code-editor.orig/vscode/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts ++++ sagemaker-code-editor/vscode/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +@@ -258,7 +258,12 @@ Registry.as(Conf + type: 'boolean', + description: localize('extensionsInQuickAccess', "When enabled, extensions can be searched for via Quick Access and report issues from there."), + default: true +- } ++ }, ++ 'extensions.openNotebookData': { ++ type: 'object', ++ scope: ConfigurationScope.APPLICATION, ++ default: {}, ++ }, + } + }); + +Index: sagemaker-code-editor/vscode/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +=================================================================== +--- sagemaker-code-editor.orig/vscode/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts ++++ sagemaker-code-editor/vscode/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +@@ -950,6 +950,17 @@ export class ExtensionsWorkbenchService + urlService.registerHandler(this); + + this.whenInitialized = this.initialize(); ++ ++ //Update workspace to open notebook ++ const urlParams = new URLSearchParams(window.location.search); ++ const notebookKey = urlParams.get('openNotebook'); ++ const clusterId = urlParams.get('clusterId'); ++ const region = urlParams.get('region'); ++ configurationService.updateValue('extensions.openNotebookData', { ++ notebookKey: notebookKey, ++ clusterId: clusterId, ++ region: region, ++ }); + } + + private async initialize(): Promise { +Index: sagemaker-code-editor/vscode/extensions/sagemaker-open-notebook-extension/extension-browser.webpack.config.js +=================================================================== +--- /dev/null ++++ sagemaker-code-editor/vscode/extensions/sagemaker-open-notebook-extension/extension-browser.webpack.config.js +@@ -0,0 +1,17 @@ ++/*--------------------------------------------------------------------------------------------- ++ * Copyright Amazon.com Inc. or its affiliates. All rights reserved. ++ * Licensed under the MIT License. See License.txt in the project root for license information. ++ *--------------------------------------------------------------------------------------------*/ ++ ++//@ts-check ++ ++'use strict'; ++ ++const withBrowserDefaults = require('../shared.webpack.config').browser; ++ ++module.exports = withBrowserDefaults({ ++ context: __dirname, ++ entry: { ++ extension: './src/extension.ts' ++ }, ++}); +Index: sagemaker-code-editor/vscode/extensions/sagemaker-open-notebook-extension/extension.webpack.config.js +=================================================================== +--- /dev/null ++++ sagemaker-code-editor/vscode/extensions/sagemaker-open-notebook-extension/extension.webpack.config.js +@@ -0,0 +1,20 @@ ++/*--------------------------------------------------------------------------------------------- ++ * Copyright Amazon.com Inc. or its affiliates. All rights reserved. ++ * Licensed under the MIT License. See License.txt in the project root for license information. ++ *--------------------------------------------------------------------------------------------*/ ++ ++//@ts-check ++ ++'use strict'; ++ ++const withDefaults = require('../shared.webpack.config'); ++ ++module.exports = withDefaults({ ++ context: __dirname, ++ resolve: { ++ mainFields: ['module', 'main'] ++ }, ++ entry: { ++ extension: './src/extension.ts', ++ } ++}); +Index: sagemaker-code-editor/vscode/extensions/sagemaker-open-notebook-extension/package.json +=================================================================== +--- /dev/null ++++ sagemaker-code-editor/vscode/extensions/sagemaker-open-notebook-extension/package.json +@@ -0,0 +1,44 @@ ++{ ++ "name": "sagemaker-open-notebook-extension", ++ "displayName": "Sagemaker open notebook Extension", ++ "description": "To download and open sample notebook when open code editor", ++ "extensionKind": [ ++ "workspace" ++ ], ++ "version": "1.0.0", ++ "publisher": "sagemaker", ++ "license": "MIT", ++ "engines": { ++ "vscode": "^1.70.0" ++ }, ++ "main": "./out/extension", ++ "categories": [ ++ "Other" ++ ], ++ "activationEvents": [ ++ "*" ++ ], ++ "capabilities": { ++ "virtualWorkspaces": true, ++ "untrustedWorkspaces": { ++ "supported": true ++ } ++ }, ++ "contributes": { ++ "configuration": { ++ "type": "object", ++ "title": "Sagemaker Open Notebook Extension", ++ "properties": {} ++ }, ++ "commands": [ ++ ] ++ }, ++ "scripts": { ++ "compile": "gulp compile-extension:sagemaker-open-notebook-extension", ++ "watch": "npm run build-preview && gulp watch-extension:sagemaker-open-notebook-extension", ++ "vscode:prepublish": "npm run build-ext", ++ "build-ext": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:sagemaker-open-notebook-extension ./tsconfig.json" ++ }, ++ "dependencies": {}, ++ "repository": {} ++} +Index: sagemaker-code-editor/vscode/extensions/sagemaker-open-notebook-extension/src/extension.ts +=================================================================== +--- /dev/null ++++ sagemaker-code-editor/vscode/extensions/sagemaker-open-notebook-extension/src/extension.ts +@@ -0,0 +1,91 @@ ++ ++import * as vscode from 'vscode'; ++import * as https from 'https'; ++import * as fs from 'fs'; ++import * as path from 'path'; ++import * as os from 'os'; ++import * as console from 'console'; ++ ++export function activate() { ++ const config = vscode.workspace.getConfiguration('extensions.openNotebookData'); ++ const notebookKey = config.get('notebookKey') as string; ++ const clusterId = config.get('clusterId') as string; ++ const region = config.get('region') as string; ++ if(notebookKey){ ++ loadAndDisplayNotebook(notebookKey, clusterId, region); ++ } ++ ++} ++ ++function isValidRegion(region: string): boolean { ++ // This regex allows for characters, numbers, and hyphens ++ const regionRegex = /^[a-zA-Z0-9-]+$/; ++ return regionRegex.test(region); ++} ++ ++async function loadAndDisplayNotebook(fileKey: string, clusterId: string, region: string) { ++ if (!isValidRegion(region)) { ++ vscode.window.showErrorMessage('Invalid region format. Region should only contain characters, numbers, and hyphens.'); ++ return; ++ } ++ ++ const bucketName = `jumpstart-cache-prod-${region}`; ++ const url = `https://${bucketName}.s3.${region}.amazonaws.com/${fileKey}`; ++ try { ++ let content = await downloadFile(url); ++ content = processNotebookContent(content, clusterId, region); ++ const tempDir = os.tmpdir(); ++ const tempFilePath = path.join(tempDir, 'downloaded-notebook.ipynb'); ++ fs.writeFileSync(tempFilePath, content); ++ const uri = vscode.Uri.file(tempFilePath); ++ await openNotebookDocument(uri); ++ } catch (error) { ++ vscode.window.showErrorMessage('Error downloading or opening notebook: ' + error.message); ++ } ++} ++ ++function processNotebookContent(content: string, clusterId: string, region: string): string { ++ const notebook = JSON.parse(content); ++ notebook.cells = notebook.cells.map((cell: any) => { ++ if (cell.metadata && ++ cell.metadata.jumpStartAlterations && ++ cell.metadata.jumpStartAlterations.includes('clusterId')) { ++ cell.source = [ ++ "%%bash\n", ++ `aws ssm start-session --target sagemaker-cluster:${clusterId} --region ${region}` ++ ]; ++ cell.cell_type = "code"; ++ } ++ return cell; ++ }); ++ return JSON.stringify(notebook, null, 2); ++} ++ ++async function openNotebookDocument(uri: vscode.Uri) { ++ try { ++ // Open the notebook document ++ const document = await vscode.workspace.openNotebookDocument(uri); ++ // Show the notebook document in a notebook editor ++ await vscode.window.showNotebookDocument(document); ++ } catch (error) { ++ console.error('Failed to open notebook:', error); ++ vscode.window.showErrorMessage('Failed to open notebook: ' + error.message); ++ } ++} ++ ++function downloadFile(url: string): Promise { ++ return new Promise((resolve, reject) => { ++ https.get(url, (response) => { ++ let data = ''; ++ response.on('data', (chunk) => { ++ data += chunk; ++ }); ++ response.on('end', () => { ++ resolve(data); ++ }); ++ }).on('error', (error) => { ++ reject(error); ++ }); ++ }); ++} ++export function deactivate() {} +Index: sagemaker-code-editor/vscode/extensions/sagemaker-open-notebook-extension/tsconfig.json +=================================================================== +--- /dev/null ++++ sagemaker-code-editor/vscode/extensions/sagemaker-open-notebook-extension/tsconfig.json +@@ -0,0 +1,10 @@ ++{ ++ "extends": "../tsconfig.base.json", ++ "compilerOptions": { ++ "outDir": "./out" ++ }, ++ "include": [ ++ "../sagemaker-open-notebook-extension/src/**/*", ++ "../../src/vscode-dts/vscode.d.ts" ++ ] ++} +Index: sagemaker-code-editor/vscode/extensions/sagemaker-open-notebook-extension/yarn.lock +=================================================================== +--- /dev/null ++++ sagemaker-code-editor/vscode/extensions/sagemaker-open-notebook-extension/yarn.lock +@@ -0,0 +1,4 @@ ++# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. ++# yarn lockfile v1 ++ ++ +Index: sagemaker-code-editor/vscode/extensions/sagemaker-open-notebook-extension/.vscodeignore +=================================================================== +--- /dev/null ++++ sagemaker-code-editor/vscode/extensions/sagemaker-open-notebook-extension/.vscodeignore +@@ -0,0 +1,12 @@ ++.vscode/** ++.vscode-test/** ++out/test/** ++out/** ++test/** ++src/** ++tsconfig.json ++out/test/** ++out/** ++cgmanifest.json ++yarn.lock ++preview-src/** +Index: sagemaker-code-editor/vscode/extensions/sagemaker-open-notebook-extension/README.md +=================================================================== +--- /dev/null ++++ sagemaker-code-editor/vscode/extensions/sagemaker-open-notebook-extension/README.md +@@ -0,0 +1,18 @@ ++# Code Editor Open Notebook Extension ++ ++The Open Notebook extension enables users to download, transform, and display sample notebooks from a public Amazon S3 bucket owned by the SageMaker team. This extension streamlines the process of accessing and working with SageMaker sample notebooks directly within Code Editor. ++ ++## Features ++ ++- Download sample notebooks from a specified S3 bucket ++- Transform notebooks for compatibility with VSCode ++- Display notebooks within the Code Editor environment ++- Utilize URL parameters to open specific notebooks ++ ++## Usage ++ ++The extension uses parameters from the URL to open the desired notebook. The required parameters are: ++- Notebook key: The identifier for the specific notebook in the S3 bucket ++- Cluster ID: The ID of the SageMaker cluster ++- Region: The AWS region where the S3 bucket is located ++ diff --git a/patches/series b/patches/series index 9091c133..dfb7a72a 100644 --- a/patches/series +++ b/patches/series @@ -1,3 +1,4 @@ +update-csp.patch sagemaker-extension.diff disable-online-services.diff disable-telemetry.diff @@ -7,4 +8,7 @@ local-storage.diff sagemaker-integration.diff license.diff sagemaker-idle-extension.patch -terminal-crash-mitigation.patch \ No newline at end of file +terminal-crash-mitigation.patch +sagemaker-open-notebook-extension.patch +display-language.diff +sagemaker-extensions-sync.patch