From 700dcd2668805f21e04e1d432e25fa658e802ec2 Mon Sep 17 00:00:00 2001 From: Samuel Pangestu Date: Thu, 10 Apr 2025 00:21:46 -0700 Subject: [PATCH 1/7] init patch and impl initial patch and implementation up to getting the unsynced extensions and showing them in a dropdown menu --- patches/sagemaker-extensions-sync.patch | 490 ++++++++++++++++++++++++ patches/series | 1 + 2 files changed, 491 insertions(+) create mode 100644 patches/sagemaker-extensions-sync.patch diff --git a/patches/sagemaker-extensions-sync.patch b/patches/sagemaker-extensions-sync.patch new file mode 100644 index 000000000..e4bcc4eec --- /dev/null +++ b/patches/sagemaker-extensions-sync.patch @@ -0,0 +1,490 @@ +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 Extension 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-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,288 @@ ++import * as fs from "fs/promises"; ++import * as path from "path"; ++import * as process from "process"; ++import * as vscode from 'vscode'; ++import { execFile } from "child_process"; ++import { promisify } from "util"; ++ ++import { ++ ExtensionInfo, ++ IMAGE_EXTENSIONS_DIR, ++ LOG_PREFIX, ++ PERSISTENT_VOLUME_EXTENSIONS_DIR, ++} from "./constants" ++ ++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); ++ const stats = await fs.stat(itemPath); ++ ++ if (stats.isDirectory()) { ++ const packageJsonPath = path.join(itemPath, "package.json"); ++ ++ try { ++ 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) { ++ console.error(`${LOG_PREFIX} Error reading package.json in ${itemPath}:`, error); ++ } ++ } ++ } ++ } catch (error) { ++ console.error(`${LOG_PREFIX} Error reading directory ${directoryPath}:`, error); ++ } ++ return results; ++} ++ ++async function getInstalledExtensions(): Promise { ++ const command = "./scripts/code-server.sh"; ++ // todo: uncomment correct code ++ //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; ++ } ++} ++ ++async function installExtension(extensionId: string, ++ prePackagedExtensionInfo: ExtensionInfo, ++ installedExtensionInfo?: ExtensionInfo | undefined ++): Promise { ++ console.log(`${LOG_PREFIX} copying ${prePackagedExtensionInfo.path} and removing ${installedExtensionInfo?.path}`); ++} ++ ++export async function activate(context: vscode.ExtensionContext) { ++ ++ // this extension will only activate within a sagemaker app ++ const isSageMakerApp = !!process.env?.SAGEMAKER_APP_TYPE_LOWERCASE; ++ if (!isSageMakerApp) { ++ return; ++ } else { ++ vscode.window.showInformationMessage(`App type: ${process.env.SAGEMAKER_APP_TYPE_LOWERCASE}`); ++ } ++ ++ const prePackagedExtensions = await getExtensionsFromDirectory("/Users/pangestu/.vscode-server-oss-dev/extensions"); ++ // todo: uncomment correct code ++ // 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 => { ++ pvExtensionsByName[extension.name] = extension; ++ pvExtensionsById[extension.identifier] = extension; ++ }); ++ console.log(`${LOG_PREFIX} Found extensions in persistent volume: `, JSON.stringify(pvExtensionsByName, null, 2)); ++ ++ // get installed extensions. this could be different from pvExtensions b/c vscode 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)); ++ ++ // 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} ++ // todo: remove the clear below ++ installedExtensions.clear(); ++ 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: `, JSON.stringify(unsyncedExtensions, null, 2)); ++ ++ if (unsyncedExtensions) { ++ 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: `Currently installed version: ${unsyncedExtensions[extensionId]}` ++ })); ++ quickPick.placeholder = 'Select extensions to install'; ++ quickPick.canSelectMany = true; ++ quickPick.ignoreFocusOut = true; ++ ++ quickPick.onDidAccept(() => { ++ const selectedExtensions = quickPick.selectedItems.map(item => item.label); ++ selectedExtensions.forEach(async extensionId => { ++ await installExtension(extensionId, prePackagedExtensionsById[extensionId], pvExtensionsById[extensionId]); ++ }); ++ quickPick.hide(); ++ vscode.window.showInformationMessage(`You selected: ${selectedExtensions.join(', ')}`, { modal: true }); ++ }); ++ ++ quickPick.show(); ++ } ++ } ++} ++ ++// export function activate(context: vscode.ExtensionContext) { ++ ++// // this extension will only activate within a sagemaker app ++// const isSageMakerApp = !!process.env?.SAGEMAKER_APP_TYPE_LOWERCASE; ++// if (!isSageMakerApp) { ++// return; ++// } else { ++// vscode.window.showInformationMessage(`App type: ${process.env.SAGEMAKER_APP_TYPE_LOWERCASE}`); ++// } ++ ++// let prePackagedExtensions: ExtensionInfo[] = []; ++// getExtensionsFromDirectory(IMAGE_EXTENSIONS_DIR).then( ++// extensions => { ++// prePackagedExtensions = extensions; ++// console.log("Found pre-packaged extensions: ", extensions); ++// } ++// ); ++ ++// // index by extension name only, e.g. jupyter ++// const pvExtensionsByName: Record = {} ++ ++// //index by full extension identifier, e.g. ms-toolsai.jupyter@2024.5.0 ++// const pvExtensionsByNameVersion: Record = {}; ++ ++// // getExtensionsFromDirectory(PERSISTENT_VOLUME_EXTENSIONS_DIR).then( ++// getExtensionsFromDirectory("/Users/pangestu/.vscode-server-oss-dev/extensions").then( ++// extensions => { ++// extensions.forEach(extension => { ++// pvExtensionsByName[extension.name] = extension; ++// pvExtensionsByNameVersion[extension.identifier] = extension; ++// }); ++// console.log("Found extensions in persistent volume: ", pvExtensionsByNameVersion); ++// } ++// ); ++ ++// let installedExtensions: Set; ++// getInstalledExtensions().then( ++// extensions => { ++// installedExtensions = new Set(extensions); ++// console.log("Found installed extensions: ", extensions); ++// } ++// ) ++ ++// // 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} ++// installedExtensions = new Set(); ++// const unsyncedExtensions: Record = {} ++// prePackagedExtensions.forEach(extension => { ++// const id = extension.identifier; ++// if (!(id in installedExtensions)){ ++// unsyncedExtensions[id] = pvExtensionsByName[extension.name]?.version; ++// } ++// }); ++// console.log("Unsynced extensions: ", unsyncedExtensions); ++ ++// const showErrorNotification = vscode.commands.registerCommand('notifications-sample.showError', () => { ++// showExtensionsQuickPick(); ++// }); ++ ++// vscode.window.showErrorMessage('Error Notification'); ++ ++// // Notification with actions ++// const showWarningNotificationWithActions = vscode.commands.registerCommand('notifications-sample.showWarningWithActions', async () => { ++// 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 extensions = ['ms-toolsai.jupyter@2023.9.100 (current version: 2022.12.100)', ++// 'amazonwebservices.aws-toolkit-vscode@3.30.0 (current version: n/a)', ++// 'ms-toolsai.jupyter-renderers@1.0.19 (current version: 1.0.14)']; ++// const quickPick = vscode.window.createQuickPick(); ++// quickPick.items = extensions.map(label => ({ label })); ++// quickPick.placeholder = 'Select extensions to install'; ++// quickPick.canSelectMany = true; ++// quickPick.ignoreFocusOut = true; ++ ++// quickPick.onDidAccept(() => { ++// const selectedExtensions = quickPick.selectedItems.map(item => item.label); ++// vscode.window.showInformationMessage(`You selected: ${selectedExtensions.join(', ')}`, { modal: true }); ++// quickPick.hide(); ++// }); ++ ++// quickPick.show(); ++// } else if (selection === "Dismiss") { ++// vscode.window.showInformationMessage('You dismissed the synchronization.', { modal: true }); ++// } ++ ++// }); ++ ++// // Progress notification with option to cancel ++// const showProgressNotification = vscode.commands.registerCommand('notifications-sample.showProgress', () => { ++// vscode.window.withProgress({ ++// location: vscode.ProgressLocation.Notification, ++// title: "Progress Notification", ++// cancellable: true ++// }, (progress, token) => { ++// token.onCancellationRequested(() => { ++// console.log("User canceled the long running operation"); ++// }); ++ ++// progress.report({ increment: 0 }); ++ ++// setTimeout(() => { ++// progress.report({ increment: 10, message: "Still going..." }); ++// }, 1000); ++ ++// setTimeout(() => { ++// progress.report({ increment: 40, message: "Still going even more..." }); ++// }, 2000); ++ ++// setTimeout(() => { ++// progress.report({ increment: 50, message: "I am long running! - almost there..." }); ++// }, 3000); ++ ++// const p = new Promise(resolve => { ++// setTimeout(() => { ++// resolve(); ++// }, 5000); ++// }); ++ ++// return p; ++// }); ++// }); ++ ++// // Show all notifications to show do not disturb behavior ++// const showAllNotifications = vscode.commands.registerCommand('notifications-sample.showAll', () => { ++// vscode.commands.executeCommand('notifications-sample.showInfo'); ++// vscode.commands.executeCommand('notifications-sample.showWarning'); ++// vscode.commands.executeCommand('notifications-sample.showWarningWithActions'); ++// vscode.commands.executeCommand('notifications-sample.showError'); ++// vscode.commands.executeCommand('notifications-sample.showProgress'); ++// vscode.commands.executeCommand('notifications-sample.showInfoAsModal'); ++// }); ++ ++// context.subscriptions.push(showErrorNotification, showProgressNotification, showWarningNotificationWithActions, showAllNotifications); ++// } +\ 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 ++ ++ diff --git a/patches/series b/patches/series index 240a749b3..b6f476ca0 100644 --- a/patches/series +++ b/patches/series @@ -10,3 +10,4 @@ sagemaker-idle-extension.patch terminal-crash-mitigation.patch sagemaker-open-notebook-extension.patch display-language.diff +sagemaker-extensions-sync.patch From 450f91913e2fb6ef72bc271026625ddfc35653f7 Mon Sep 17 00:00:00 2001 From: Samuel Pangestu Date: Thu, 10 Apr 2025 19:13:16 -0700 Subject: [PATCH 2/7] Add installation logic Add installation logic. Everything should work now --- patches/sagemaker-extensions-sync.patch | 324 +++++++++++------------- patches/series | 1 + 2 files changed, 142 insertions(+), 183 deletions(-) diff --git a/patches/sagemaker-extensions-sync.patch b/patches/sagemaker-extensions-sync.patch index e4bcc4eec..213b2e1e8 100644 --- a/patches/sagemaker-extensions-sync.patch +++ b/patches/sagemaker-extensions-sync.patch @@ -46,7 +46,7 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/README. @@ -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. ++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 =================================================================== @@ -102,7 +102,7 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/package @@ -0,0 +1,44 @@ +{ + "name": "sagemaker-extensions-sync", -+ "displayName": "SageMaker Extension Sync", ++ "displayName": "SageMaker Extensions Sync", + "description": "Sync pre-packaged extensions from SageMaker Distribution", + "extensionKind": [ + "workspace" @@ -148,10 +148,13 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/con =================================================================== --- /dev/null +++ sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/constants.ts -@@ -0,0 +1,21 @@ +@@ -0,0 +1,24 @@ +// constants -+export const PERSISTENT_VOLUME_EXTENSIONS_DIR = "/home/sagemaker-user/sagemaker-code-editor-server-data/extensions"; -+export const IMAGE_EXTENSIONS_DIR = "/opt/amazon/sagemaker-code-editor-server-data/extensions"; ++//todo: uncomment correct code ++// export const PERSISTENT_VOLUME_EXTENSIONS_DIR = "/home/sagemaker-user/sagemaker-code-editor-server-data/extensions"; ++export const PERSISTENT_VOLUME_EXTENSIONS_DIR = "/Users/pangestu/.vscode-server-oss-dev/extensions"; ++// export const IMAGE_EXTENSIONS_DIR = "/opt/amazon/sagemaker-code-editor-server-data/extensions"; ++export const IMAGE_EXTENSIONS_DIR = "/tmp/extensions"; +export const LOG_PREFIX = "[sagemaker-extensions-sync]" + +export class ExtensionInfo { @@ -174,13 +177,13 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/ext =================================================================== --- /dev/null +++ sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/extension.ts -@@ -0,0 +1,288 @@ +@@ -0,0 +1,243 @@ +import * as fs from "fs/promises"; +import * as path from "path"; +import * as process from "process"; +import * as vscode from 'vscode'; +import { execFile } from "child_process"; -+import { promisify } from "util"; ++import { promisify } from "util"; + +import { + ExtensionInfo, @@ -196,12 +199,12 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/ext + + for (const item of items) { + const itemPath = path.join(directoryPath, item); -+ const stats = await fs.stat(itemPath); ++ try { ++ const stats = await fs.stat(itemPath); + -+ if (stats.isDirectory()) { -+ const packageJsonPath = path.join(itemPath, "package.json"); ++ if (stats.isDirectory()) { ++ const packageJsonPath = path.join(itemPath, "package.json"); + -+ try { + const packageData = JSON.parse(await fs.readFile(packageJsonPath, "utf8")); + + if (packageData.name && packageData.publisher && packageData.version) { @@ -212,9 +215,10 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/ext + itemPath, + )); + } -+ } catch (error) { -+ console.error(`${LOG_PREFIX} Error reading package.json in ${itemPath}:`, error); + } ++ } 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) { @@ -224,12 +228,12 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/ext +} + +async function getInstalledExtensions(): Promise { -+ const command = "./scripts/code-server.sh"; ++ const command = "./scripts/code-server.sh"; + // todo: uncomment correct code + //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); @@ -243,11 +247,93 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/ext + } +} + -+async function installExtension(extensionId: string, -+ prePackagedExtensionInfo: ExtensionInfo, -+ installedExtensionInfo?: ExtensionInfo | undefined ++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); ++ } ++ } ++} ++ ++async function installExtension( ++ prePackagedExtensionInfo: ExtensionInfo, installedExtensionInfo?: ExtensionInfo | undefined +): Promise { -+ console.log(`${LOG_PREFIX} copying ${prePackagedExtensionInfo.path} and removing ${installedExtensionInfo?.path}`); ++ 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}`); ++ } +} + +export async function activate(context: vscode.ExtensionContext) { @@ -256,13 +342,14 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/ext + const isSageMakerApp = !!process.env?.SAGEMAKER_APP_TYPE_LOWERCASE; + if (!isSageMakerApp) { + return; -+ } else { -+ vscode.window.showInformationMessage(`App type: ${process.env.SAGEMAKER_APP_TYPE_LOWERCASE}`); + } + -+ const prePackagedExtensions = await getExtensionsFromDirectory("/Users/pangestu/.vscode-server-oss-dev/extensions"); -+ // todo: uncomment correct code -+ // const prePackagedExtensions: ExtensionInfo[] = await getExtensionsFromDirectory(IMAGE_EXTENSIONS_DIR); ++ // get installed extensions. this could be different from pvExtensions b/c vscode 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; @@ -274,20 +361,15 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/ext + const pvExtensionsByName: Record = {}; + const pvExtensionsById: Record = {}; + pvExtensions.forEach(extension => { -+ pvExtensionsByName[extension.name] = extension; -+ pvExtensionsById[extension.identifier] = 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 extensions in persistent volume: `, JSON.stringify(pvExtensionsByName, null, 2)); -+ -+ // get installed extensions. this could be different from pvExtensions b/c vscode 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)); ++ console.log(`${LOG_PREFIX} Found extensions in persistent volume: `, JSON.stringify(pvExtensionsById, null, 2)); + + // 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} -+ // todo: remove the clear below -+ installedExtensions.clear(); + const unsyncedExtensions: Record = {} + prePackagedExtensions.forEach(extension => { + const id = extension.identifier; @@ -297,172 +379,48 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/ext + }); + console.log(`${LOG_PREFIX} Unsynced extensions: `, JSON.stringify(unsyncedExtensions, null, 2)); + -+ if (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?', ++ '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: `Currently installed version: ${unsyncedExtensions[extensionId]}` ++ 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(() => { ++ quickPick.onDidAccept(async () => { + const selectedExtensions = quickPick.selectedItems.map(item => item.label); -+ selectedExtensions.forEach(async extensionId => { -+ await installExtension(extensionId, prePackagedExtensionsById[extensionId], pvExtensionsById[extensionId]); -+ }); ++ await Promise.all( ++ selectedExtensions.map(extensionId => { ++ const extensionName = prePackagedExtensionsById[extensionId].name; ++ installExtension(prePackagedExtensionsById[extensionId], pvExtensionsByName[extensionName]); ++ }) ++ ); ++ await refreshExtensionsMetadata(); ++ + quickPick.hide(); -+ vscode.window.showInformationMessage(`You selected: ${selectedExtensions.join(', ')}`, { modal: true }); ++ await vscode.window.showInformationMessage( ++ 'Extensions have been installed. Would you like to reload the window?', ++ { modal: true }, ++ 'Reload' ++ ).then(selection => { ++ if (selection === 'Reload') { ++ vscode.commands.executeCommand('workbench.action.reloadWindow'); ++ } ++ }); + }); + + quickPick.show(); -+ } ++ } + } +} -+ -+// export function activate(context: vscode.ExtensionContext) { -+ -+// // this extension will only activate within a sagemaker app -+// const isSageMakerApp = !!process.env?.SAGEMAKER_APP_TYPE_LOWERCASE; -+// if (!isSageMakerApp) { -+// return; -+// } else { -+// vscode.window.showInformationMessage(`App type: ${process.env.SAGEMAKER_APP_TYPE_LOWERCASE}`); -+// } -+ -+// let prePackagedExtensions: ExtensionInfo[] = []; -+// getExtensionsFromDirectory(IMAGE_EXTENSIONS_DIR).then( -+// extensions => { -+// prePackagedExtensions = extensions; -+// console.log("Found pre-packaged extensions: ", extensions); -+// } -+// ); -+ -+// // index by extension name only, e.g. jupyter -+// const pvExtensionsByName: Record = {} -+ -+// //index by full extension identifier, e.g. ms-toolsai.jupyter@2024.5.0 -+// const pvExtensionsByNameVersion: Record = {}; -+ -+// // getExtensionsFromDirectory(PERSISTENT_VOLUME_EXTENSIONS_DIR).then( -+// getExtensionsFromDirectory("/Users/pangestu/.vscode-server-oss-dev/extensions").then( -+// extensions => { -+// extensions.forEach(extension => { -+// pvExtensionsByName[extension.name] = extension; -+// pvExtensionsByNameVersion[extension.identifier] = extension; -+// }); -+// console.log("Found extensions in persistent volume: ", pvExtensionsByNameVersion); -+// } -+// ); -+ -+// let installedExtensions: Set; -+// getInstalledExtensions().then( -+// extensions => { -+// installedExtensions = new Set(extensions); -+// console.log("Found installed extensions: ", extensions); -+// } -+// ) -+ -+// // 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} -+// installedExtensions = new Set(); -+// const unsyncedExtensions: Record = {} -+// prePackagedExtensions.forEach(extension => { -+// const id = extension.identifier; -+// if (!(id in installedExtensions)){ -+// unsyncedExtensions[id] = pvExtensionsByName[extension.name]?.version; -+// } -+// }); -+// console.log("Unsynced extensions: ", unsyncedExtensions); -+ -+// const showErrorNotification = vscode.commands.registerCommand('notifications-sample.showError', () => { -+// showExtensionsQuickPick(); -+// }); -+ -+// vscode.window.showErrorMessage('Error Notification'); -+ -+// // Notification with actions -+// const showWarningNotificationWithActions = vscode.commands.registerCommand('notifications-sample.showWarningWithActions', async () => { -+// 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 extensions = ['ms-toolsai.jupyter@2023.9.100 (current version: 2022.12.100)', -+// 'amazonwebservices.aws-toolkit-vscode@3.30.0 (current version: n/a)', -+// 'ms-toolsai.jupyter-renderers@1.0.19 (current version: 1.0.14)']; -+// const quickPick = vscode.window.createQuickPick(); -+// quickPick.items = extensions.map(label => ({ label })); -+// quickPick.placeholder = 'Select extensions to install'; -+// quickPick.canSelectMany = true; -+// quickPick.ignoreFocusOut = true; -+ -+// quickPick.onDidAccept(() => { -+// const selectedExtensions = quickPick.selectedItems.map(item => item.label); -+// vscode.window.showInformationMessage(`You selected: ${selectedExtensions.join(', ')}`, { modal: true }); -+// quickPick.hide(); -+// }); -+ -+// quickPick.show(); -+// } else if (selection === "Dismiss") { -+// vscode.window.showInformationMessage('You dismissed the synchronization.', { modal: true }); -+// } -+ -+// }); -+ -+// // Progress notification with option to cancel -+// const showProgressNotification = vscode.commands.registerCommand('notifications-sample.showProgress', () => { -+// vscode.window.withProgress({ -+// location: vscode.ProgressLocation.Notification, -+// title: "Progress Notification", -+// cancellable: true -+// }, (progress, token) => { -+// token.onCancellationRequested(() => { -+// console.log("User canceled the long running operation"); -+// }); -+ -+// progress.report({ increment: 0 }); -+ -+// setTimeout(() => { -+// progress.report({ increment: 10, message: "Still going..." }); -+// }, 1000); -+ -+// setTimeout(() => { -+// progress.report({ increment: 40, message: "Still going even more..." }); -+// }, 2000); -+ -+// setTimeout(() => { -+// progress.report({ increment: 50, message: "I am long running! - almost there..." }); -+// }, 3000); -+ -+// const p = new Promise(resolve => { -+// setTimeout(() => { -+// resolve(); -+// }, 5000); -+// }); -+ -+// return p; -+// }); -+// }); -+ -+// // Show all notifications to show do not disturb behavior -+// const showAllNotifications = vscode.commands.registerCommand('notifications-sample.showAll', () => { -+// vscode.commands.executeCommand('notifications-sample.showInfo'); -+// vscode.commands.executeCommand('notifications-sample.showWarning'); -+// vscode.commands.executeCommand('notifications-sample.showWarningWithActions'); -+// vscode.commands.executeCommand('notifications-sample.showError'); -+// vscode.commands.executeCommand('notifications-sample.showProgress'); -+// vscode.commands.executeCommand('notifications-sample.showInfoAsModal'); -+// }); -+ -+// context.subscriptions.push(showErrorNotification, showProgressNotification, showWarningNotificationWithActions, showAllNotifications); -+// } \ No newline at end of file Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/tsconfig.json =================================================================== diff --git a/patches/series b/patches/series index b6f476ca0..dfb7a72a9 100644 --- a/patches/series +++ b/patches/series @@ -1,3 +1,4 @@ +update-csp.patch sagemaker-extension.diff disable-online-services.diff disable-telemetry.diff From ffb9c1f3ed5c36b4de729986d04d745a623c3fa0 Mon Sep 17 00:00:00 2001 From: Samuel Pangestu Date: Thu, 10 Apr 2025 19:42:55 -0700 Subject: [PATCH 3/7] refactor --- patches/sagemaker-extensions-sync.patch | 134 +++++++++++++++++++++++- 1 file changed, 132 insertions(+), 2 deletions(-) diff --git a/patches/sagemaker-extensions-sync.patch b/patches/sagemaker-extensions-sync.patch index 213b2e1e8..efc430135 100644 --- a/patches/sagemaker-extensions-sync.patch +++ b/patches/sagemaker-extensions-sync.patch @@ -177,10 +177,140 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/ext =================================================================== --- /dev/null +++ sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/extension.ts -@@ -0,0 +1,243 @@ +@@ -0,0 +1,101 @@ ++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 extensions in persistent volume: `, JSON.stringify(pvExtensionsById, null, 2)); ++ ++ // 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: `, JSON.stringify(unsyncedExtensions, null, 2)); ++ ++ 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); ++ await Promise.all( ++ selectedExtensions.map(extensionId => { ++ const extensionName = prePackagedExtensionsById[extensionId].name; ++ 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,155 @@ +import * as fs from "fs/promises"; +import * as path from "path"; -+import * as process from "process"; +import * as vscode from 'vscode'; +import { execFile } from "child_process"; +import { promisify } from "util"; From 279303ab41f48c43b81276cac939dbe706e7eb94 Mon Sep 17 00:00:00 2001 From: Samuel Pangestu Date: Thu, 10 Apr 2025 19:49:19 -0700 Subject: [PATCH 4/7] Remove parallel installs remove parallel installs due to race condition with writing obsolete file --- patches/sagemaker-extensions-sync.patch | 136 +++--------------------- 1 file changed, 12 insertions(+), 124 deletions(-) diff --git a/patches/sagemaker-extensions-sync.patch b/patches/sagemaker-extensions-sync.patch index efc430135..d0f673699 100644 --- a/patches/sagemaker-extensions-sync.patch +++ b/patches/sagemaker-extensions-sync.patch @@ -177,7 +177,7 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/ext =================================================================== --- /dev/null +++ sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/extension.ts -@@ -0,0 +1,101 @@ +@@ -0,0 +1,100 @@ +import * as process from "process"; +import * as vscode from 'vscode'; + @@ -224,7 +224,7 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/ext + pvExtensionsById[extension.identifier] = extension; + } + }); -+ console.log(`${LOG_PREFIX} Found extensions in persistent volume: `, JSON.stringify(pvExtensionsById, null, 2)); ++ 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} @@ -235,7 +235,7 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/ext + unsyncedExtensions[id] = pvExtensionsByName[extension.name]?.version ?? null; + } + }); -+ console.log(`${LOG_PREFIX} Unsynced extensions: `, JSON.stringify(unsyncedExtensions, null, 2)); ++ console.log(`${LOG_PREFIX} Unsynced extensions: `, unsyncedExtensions); + + if (Object.keys(unsyncedExtensions).length !== 0) { + const selection = await vscode.window.showWarningMessage( @@ -255,12 +255,11 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/ext + + quickPick.onDidAccept(async () => { + const selectedExtensions = quickPick.selectedItems.map(item => item.label); -+ await Promise.all( -+ selectedExtensions.map(extensionId => { -+ const extensionName = prePackagedExtensionsById[extensionId].name; -+ installExtension(prePackagedExtensionsById[extensionId], pvExtensionsByName[extensionName]); -+ }) -+ ); ++ ++ for (const extensionId of selectedExtensions) { ++ const extensionName = prePackagedExtensionsById[extensionId].name; ++ await installExtension(prePackagedExtensionsById[extensionId], pvExtensionsByName[extensionName]); ++ } + await refreshExtensionsMetadata(); + + quickPick.hide(); @@ -317,12 +316,11 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/uti + +import { + ExtensionInfo, -+ IMAGE_EXTENSIONS_DIR, + LOG_PREFIX, + PERSISTENT_VOLUME_EXTENSIONS_DIR, +} from "./constants" + -+async function getExtensionsFromDirectory(directoryPath: string): Promise { ++export async function getExtensionsFromDirectory(directoryPath: string): Promise { + const results: ExtensionInfo[] = []; + try { + const items = await fs.readdir(directoryPath); @@ -357,7 +355,7 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/uti + return results; +} + -+async function getInstalledExtensions(): Promise { ++export async function getInstalledExtensions(): Promise { + const command = "./scripts/code-server.sh"; + // todo: uncomment correct code + //const command = "sagemaker-code-editor"; @@ -377,7 +375,7 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/uti + } +} + -+async function refreshExtensionsMetadata(): Promise { ++export async function refreshExtensionsMetadata(): Promise { + const metaDataFile = path.join(PERSISTENT_VOLUME_EXTENSIONS_DIR, "extensions.json"); + try { + await fs.unlink(metaDataFile); @@ -388,7 +386,7 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/uti + } +} + -+async function installExtension( ++export async function installExtension( + prePackagedExtensionInfo: ExtensionInfo, installedExtensionInfo?: ExtensionInfo | undefined +): Promise { + if (installedExtensionInfo) { @@ -465,114 +463,4 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/uti + console.error(`${LOG_PREFIX} ${error}`); + } +} -+ -+export async function activate(context: vscode.ExtensionContext) { -+ -+ // 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 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 extensions in persistent volume: `, JSON.stringify(pvExtensionsById, null, 2)); -+ -+ // 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: `, JSON.stringify(unsyncedExtensions, null, 2)); -+ -+ 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); -+ await Promise.all( -+ selectedExtensions.map(extensionId => { -+ const extensionName = prePackagedExtensionsById[extensionId].name; -+ installExtension(prePackagedExtensionsById[extensionId], pvExtensionsByName[extensionName]); -+ }) -+ ); -+ await refreshExtensionsMetadata(); -+ -+ quickPick.hide(); -+ await vscode.window.showInformationMessage( -+ 'Extensions have been installed. Would 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 -+ -+ From cb8b16c264283f4daf38612266bb39e4495bf7f5 Mon Sep 17 00:00:00 2001 From: Samuel Pangestu Date: Thu, 10 Apr 2025 20:13:53 -0700 Subject: [PATCH 5/7] remove testing code --- patches/sagemaker-extensions-sync.patch | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/patches/sagemaker-extensions-sync.patch b/patches/sagemaker-extensions-sync.patch index d0f673699..5efe0b7d5 100644 --- a/patches/sagemaker-extensions-sync.patch +++ b/patches/sagemaker-extensions-sync.patch @@ -148,14 +148,11 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/con =================================================================== --- /dev/null +++ sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/constants.ts -@@ -0,0 +1,24 @@ +@@ -0,0 +1,21 @@ +// constants -+//todo: uncomment correct code -+// export const PERSISTENT_VOLUME_EXTENSIONS_DIR = "/home/sagemaker-user/sagemaker-code-editor-server-data/extensions"; -+export const PERSISTENT_VOLUME_EXTENSIONS_DIR = "/Users/pangestu/.vscode-server-oss-dev/extensions"; -+// export const IMAGE_EXTENSIONS_DIR = "/opt/amazon/sagemaker-code-editor-server-data/extensions"; -+export const IMAGE_EXTENSIONS_DIR = "/tmp/extensions"; -+export const LOG_PREFIX = "[sagemaker-extensions-sync]" ++export const PERSISTENT_VOLUME_EXTENSIONS_DIR = "/home/sagemaker-user/sagemaker-code-editor-server-data/extensions"; ++export const IMAGE_EXTENSIONS_DIR = "/opt/amazon/sagemaker-code-editor-server-data/extensions"; ++export const LOG_PREFIX = "[sagemaker-extensions-sync]"; + +export class ExtensionInfo { + constructor( @@ -307,7 +304,7 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/uti =================================================================== --- /dev/null +++ sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/utils.ts -@@ -0,0 +1,155 @@ +@@ -0,0 +1,152 @@ +import * as fs from "fs/promises"; +import * as path from "path"; +import * as vscode from 'vscode'; @@ -356,11 +353,8 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/uti +} + +export async function getInstalledExtensions(): Promise { -+ const command = "./scripts/code-server.sh"; -+ // todo: uncomment correct code -+ //const command = "sagemaker-code-editor"; -+ const args = ["--list-extensions", "--show-versions", ]; -+ // "--extensions-dir", PERSISTENT_VOLUME_EXTENSIONS_DIR]; ++ const command = "sagemaker-code-editor"; ++ const args = ["--list-extensions", "--show-versions", "--extensions-dir", PERSISTENT_VOLUME_EXTENSIONS_DIR]; + + const execFileAsync = promisify(execFile); + try { From f5d6d7191c7b3d502c36de7244eb4177ae06de0b Mon Sep 17 00:00:00 2001 From: Samuel Pangestu Date: Thu, 10 Apr 2025 23:26:17 -0700 Subject: [PATCH 6/7] Fix image extensions directory path --- patches/sagemaker-extensions-sync.patch | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patches/sagemaker-extensions-sync.patch b/patches/sagemaker-extensions-sync.patch index 5efe0b7d5..2d6e6315b 100644 --- a/patches/sagemaker-extensions-sync.patch +++ b/patches/sagemaker-extensions-sync.patch @@ -151,7 +151,7 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/con @@ -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-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 { From 3543ce6739471ad908ace52fc4b26a86610b7d71 Mon Sep 17 00:00:00 2001 From: Samuel Pangestu Date: Fri, 11 Apr 2025 00:52:45 -0700 Subject: [PATCH 7/7] Update patched-vscode --- patched-vscode/build/gulpfile.extensions.js | 1 + patched-vscode/build/npm/dirs.js | 1 + .../sagemaker-extensions-sync/.vscodeignore | 12 ++ .../sagemaker-extensions-sync/README.md | 3 + .../extension-browser.webpack.config.js | 17 ++ .../extension.webpack.config.js | 20 +++ .../sagemaker-extensions-sync/package.json | 44 +++++ .../src/constants.ts | 21 +++ .../src/extension.ts | 100 ++++++++++++ .../sagemaker-extensions-sync/src/utils.ts | 152 ++++++++++++++++++ .../sagemaker-extensions-sync/tsconfig.json | 10 ++ .../sagemaker-extensions-sync/yarn.lock | 4 + 12 files changed, 385 insertions(+) create mode 100644 patched-vscode/extensions/sagemaker-extensions-sync/.vscodeignore create mode 100644 patched-vscode/extensions/sagemaker-extensions-sync/README.md create mode 100644 patched-vscode/extensions/sagemaker-extensions-sync/extension-browser.webpack.config.js create mode 100644 patched-vscode/extensions/sagemaker-extensions-sync/extension.webpack.config.js create mode 100644 patched-vscode/extensions/sagemaker-extensions-sync/package.json create mode 100644 patched-vscode/extensions/sagemaker-extensions-sync/src/constants.ts create mode 100644 patched-vscode/extensions/sagemaker-extensions-sync/src/extension.ts create mode 100644 patched-vscode/extensions/sagemaker-extensions-sync/src/utils.ts create mode 100644 patched-vscode/extensions/sagemaker-extensions-sync/tsconfig.json create mode 100644 patched-vscode/extensions/sagemaker-extensions-sync/yarn.lock diff --git a/patched-vscode/build/gulpfile.extensions.js b/patched-vscode/build/gulpfile.extensions.js index d5d57f6d2..3ec358902 100644 --- a/patched-vscode/build/gulpfile.extensions.js +++ b/patched-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', diff --git a/patched-vscode/build/npm/dirs.js b/patched-vscode/build/npm/dirs.js index ae459ee0b..9f057b3a6 100644 --- a/patched-vscode/build/npm/dirs.js +++ b/patched-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', diff --git a/patched-vscode/extensions/sagemaker-extensions-sync/.vscodeignore b/patched-vscode/extensions/sagemaker-extensions-sync/.vscodeignore new file mode 100644 index 000000000..56b78554c --- /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 000000000..b8dd030e4 --- /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 000000000..68271e0e9 --- /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 000000000..598526267 --- /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 000000000..a0761fa03 --- /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 000000000..1a7fdcb84 --- /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 000000000..f9f44fd56 --- /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 000000000..e2d34fe06 --- /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 000000000..e474d9a56 --- /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 000000000..fb57ccd13 --- /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 + +