Skip to content

Commit 1b62e92

Browse files
committed
Update patched-vscode
1 parent 4f36d72 commit 1b62e92

File tree

12 files changed

+385
-0
lines changed

12 files changed

+385
-0
lines changed

patched-vscode/build/gulpfile.extensions.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ const compilations = [
6262
'extensions/simple-browser/tsconfig.json',
6363
'extensions/sagemaker-extension/tsconfig.json',
6464
'extensions/sagemaker-idle-extension/tsconfig.json',
65+
'extensions/sagemaker-extensions-sync/tsconfig.json',
6566
'extensions/sagemaker-terminal-crash-mitigation/tsconfig.json',
6667
'extensions/sagemaker-open-notebook-extension/tsconfig.json',
6768
'extensions/tunnel-forwarding/tsconfig.json',

patched-vscode/build/npm/dirs.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const dirs = [
4040
'extensions/php-language-features',
4141
'extensions/references-view',
4242
'extensions/sagemaker-extension',
43+
'extensions/sagemaker-extensions-sync',
4344
'extensions/sagemaker-idle-extension',
4445
'extensions/sagemaker-terminal-crash-mitigation',
4546
'extensions/sagemaker-open-notebook-extension',
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.vscode/**
2+
.vscode-test/**
3+
out/test/**
4+
out/**
5+
test/**
6+
src/**
7+
tsconfig.json
8+
out/test/**
9+
out/**
10+
cgmanifest.json
11+
yarn.lock
12+
preview-src/**
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# SageMaker Code Editor Extensions Sync
2+
3+
Notifies users if the extensions directory is missing pre-packaged extensions from SageMaker Distribution and give them the option to sync them.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright Amazon.com Inc. or its affiliates. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
//@ts-check
7+
8+
'use strict';
9+
10+
const withBrowserDefaults = require('../shared.webpack.config').browser;
11+
12+
module.exports = withBrowserDefaults({
13+
context: __dirname,
14+
entry: {
15+
extension: './src/extension.ts'
16+
},
17+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright Amazon.com Inc. or its affiliates. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
//@ts-check
7+
8+
'use strict';
9+
10+
const withDefaults = require('../shared.webpack.config');
11+
12+
module.exports = withDefaults({
13+
context: __dirname,
14+
resolve: {
15+
mainFields: ['module', 'main']
16+
},
17+
entry: {
18+
extension: './src/extension.ts',
19+
}
20+
});
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"name": "sagemaker-extensions-sync",
3+
"displayName": "SageMaker Extensions Sync",
4+
"description": "Sync pre-packaged extensions from SageMaker Distribution",
5+
"extensionKind": [
6+
"workspace"
7+
],
8+
"version": "1.0.0",
9+
"publisher": "sagemaker",
10+
"license": "MIT",
11+
"engines": {
12+
"vscode": "^1.70.0"
13+
},
14+
"main": "./out/extension",
15+
"categories": [
16+
"Other"
17+
],
18+
"activationEvents": [
19+
"*"
20+
],
21+
"capabilities": {
22+
"virtualWorkspaces": true,
23+
"untrustedWorkspaces": {
24+
"supported": true
25+
}
26+
},
27+
"contributes": {
28+
"commands": [
29+
{
30+
"command": "extensions-sync.syncExtensions",
31+
"title": "Sync Extensions from SageMaker Distribution",
32+
"category": "Extensions Sync"
33+
}
34+
]
35+
},
36+
"scripts": {
37+
"compile": "gulp compile-extension:sagemaker-extensions-sync",
38+
"watch": "npm run build-preview && gulp watch-extension:sagemaker-extensions-sync",
39+
"vscode:prepublish": "npm run build-ext",
40+
"build-ext": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:sagemaker-idle-extension ./tsconfig.json"
41+
},
42+
"dependencies": {},
43+
"repository": {}
44+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// constants
2+
export const PERSISTENT_VOLUME_EXTENSIONS_DIR = "/home/sagemaker-user/sagemaker-code-editor-server-data/extensions";
3+
export const IMAGE_EXTENSIONS_DIR = "/opt/amazon/sagemaker/sagemaker-code-editor-server-data/extensions";
4+
export const LOG_PREFIX = "[sagemaker-extensions-sync]";
5+
6+
export class ExtensionInfo {
7+
constructor(
8+
public name: string,
9+
public publisher: string,
10+
public version: string,
11+
public path: string | null
12+
) {}
13+
14+
get identifier(): string {
15+
return `${this.publisher}.${this.name}@${this.version}`;
16+
}
17+
18+
toString(): string {
19+
return `ExtensionInfo: ${this.identifier} (${this.path})`;
20+
}
21+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import * as process from "process";
2+
import * as vscode from 'vscode';
3+
4+
import {
5+
ExtensionInfo,
6+
IMAGE_EXTENSIONS_DIR,
7+
LOG_PREFIX,
8+
PERSISTENT_VOLUME_EXTENSIONS_DIR,
9+
} from "./constants"
10+
11+
import {
12+
getExtensionsFromDirectory,
13+
getInstalledExtensions,
14+
installExtension,
15+
refreshExtensionsMetadata } from "./utils"
16+
17+
export async function activate() {
18+
19+
// this extension will only activate within a sagemaker app
20+
const isSageMakerApp = !!process.env?.SAGEMAKER_APP_TYPE_LOWERCASE;
21+
if (!isSageMakerApp) {
22+
return;
23+
}
24+
25+
// get installed extensions. this could be different from pvExtensions b/c vscode sometimes doesn't delete the assets
26+
// for an old extension when uninstalling or changing versions
27+
const installedExtensions = new Set(await getInstalledExtensions());
28+
console.log(`${LOG_PREFIX} Found installed extensions: `, Array.from(installedExtensions));
29+
30+
const prePackagedExtensions: ExtensionInfo[] = await getExtensionsFromDirectory(IMAGE_EXTENSIONS_DIR);
31+
const prePackagedExtensionsById: Record<string, ExtensionInfo> = {};
32+
prePackagedExtensions.forEach(extension => {
33+
prePackagedExtensionsById[extension.identifier] = extension;
34+
});
35+
36+
console.log(`${LOG_PREFIX} Found pre-packaged extensions: `, prePackagedExtensions);
37+
38+
const pvExtensions = await getExtensionsFromDirectory(PERSISTENT_VOLUME_EXTENSIONS_DIR);
39+
const pvExtensionsByName: Record<string, ExtensionInfo> = {};
40+
const pvExtensionsById: Record<string, ExtensionInfo> = {};
41+
pvExtensions.forEach(extension => {
42+
if (installedExtensions.has(extension.identifier)) { // only index extensions that are installed
43+
pvExtensionsByName[extension.name] = extension;
44+
pvExtensionsById[extension.identifier] = extension;
45+
}
46+
});
47+
console.log(`${LOG_PREFIX} Found installed extensions in persistent volume: `, pvExtensionsById);
48+
49+
// check each pre-packaged extension, record if it is not in installed extensions or version mismatch
50+
// store unsynced extensions as {identifier pre-packaged ext: currently installed version}
51+
const unsyncedExtensions: Record<string, string | null> = {}
52+
prePackagedExtensions.forEach(extension => {
53+
const id = extension.identifier;
54+
if (!(installedExtensions.has(id))){
55+
unsyncedExtensions[id] = pvExtensionsByName[extension.name]?.version ?? null;
56+
}
57+
});
58+
console.log(`${LOG_PREFIX} Unsynced extensions: `, unsyncedExtensions);
59+
60+
if (Object.keys(unsyncedExtensions).length !== 0) {
61+
const selection = await vscode.window.showWarningMessage(
62+
'Warning: You have unsynchronized extensions from SageMaker Distribution \
63+
which could result in incompatibilities with Code Editor. Do you want to install them?',
64+
"Synchronize Extensions", "Dismiss");
65+
66+
if (selection === "Synchronize Extensions") {
67+
const quickPick = vscode.window.createQuickPick();
68+
quickPick.items = Object.keys(unsyncedExtensions).map(extensionId => ({
69+
label: extensionId,
70+
description: unsyncedExtensions[extensionId] ? `Currently installed version: ${unsyncedExtensions[extensionId]}` : undefined,
71+
}));
72+
quickPick.placeholder = 'Select extensions to install';
73+
quickPick.canSelectMany = true;
74+
quickPick.ignoreFocusOut = true;
75+
76+
quickPick.onDidAccept(async () => {
77+
const selectedExtensions = quickPick.selectedItems.map(item => item.label);
78+
79+
for (const extensionId of selectedExtensions) {
80+
const extensionName = prePackagedExtensionsById[extensionId].name;
81+
await installExtension(prePackagedExtensionsById[extensionId], pvExtensionsByName[extensionName]);
82+
}
83+
await refreshExtensionsMetadata();
84+
85+
quickPick.hide();
86+
await vscode.window.showInformationMessage(
87+
'Extensions have been installed. \nWould you like to reload the window?',
88+
{ modal: true },
89+
'Reload'
90+
).then(selection => {
91+
if (selection === 'Reload') {
92+
vscode.commands.executeCommand('workbench.action.reloadWindow');
93+
}
94+
});
95+
});
96+
97+
quickPick.show();
98+
}
99+
}
100+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import * as fs from "fs/promises";
2+
import * as path from "path";
3+
import * as vscode from 'vscode';
4+
import { execFile } from "child_process";
5+
import { promisify } from "util";
6+
7+
import {
8+
ExtensionInfo,
9+
LOG_PREFIX,
10+
PERSISTENT_VOLUME_EXTENSIONS_DIR,
11+
} from "./constants"
12+
13+
export async function getExtensionsFromDirectory(directoryPath: string): Promise<ExtensionInfo[]> {
14+
const results: ExtensionInfo[] = [];
15+
try {
16+
const items = await fs.readdir(directoryPath);
17+
18+
for (const item of items) {
19+
const itemPath = path.join(directoryPath, item);
20+
try {
21+
const stats = await fs.stat(itemPath);
22+
23+
if (stats.isDirectory()) {
24+
const packageJsonPath = path.join(itemPath, "package.json");
25+
26+
const packageData = JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
27+
28+
if (packageData.name && packageData.publisher && packageData.version) {
29+
results.push(new ExtensionInfo(
30+
packageData.name,
31+
packageData.publisher,
32+
packageData.version,
33+
itemPath,
34+
));
35+
}
36+
}
37+
} catch (error) {
38+
// fs.stat will break on dangling simlinks. Just skip to the next file
39+
console.error(`${LOG_PREFIX} Error reading package.json in ${itemPath}:`, error);
40+
}
41+
}
42+
} catch (error) {
43+
console.error(`${LOG_PREFIX} Error reading directory ${directoryPath}:`, error);
44+
}
45+
return results;
46+
}
47+
48+
export async function getInstalledExtensions(): Promise<string[]> {
49+
const command = "sagemaker-code-editor";
50+
const args = ["--list-extensions", "--show-versions", "--extensions-dir", PERSISTENT_VOLUME_EXTENSIONS_DIR];
51+
52+
const execFileAsync = promisify(execFile);
53+
try {
54+
const { stdout, stderr } = await execFileAsync(command, args);
55+
if (stderr) {
56+
throw new Error("stderr");
57+
}
58+
return stdout.split("\n").filter(line => line.trim() !== "");
59+
} catch (error) {
60+
console.error(`${LOG_PREFIX} Error getting list of installed extensions:`, error);
61+
throw error;
62+
}
63+
}
64+
65+
export async function refreshExtensionsMetadata(): Promise<void> {
66+
const metaDataFile = path.join(PERSISTENT_VOLUME_EXTENSIONS_DIR, "extensions.json");
67+
try {
68+
await fs.unlink(metaDataFile);
69+
} catch (error) {
70+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
71+
console.error(`${LOG_PREFIX} Error removing metadata file:`, error);
72+
}
73+
}
74+
}
75+
76+
export async function installExtension(
77+
prePackagedExtensionInfo: ExtensionInfo, installedExtensionInfo?: ExtensionInfo | undefined
78+
): Promise<void> {
79+
if (installedExtensionInfo) {
80+
console.log(`${LOG_PREFIX} Upgrading extension from ${installedExtensionInfo.identifier} to ${prePackagedExtensionInfo.identifier}`);
81+
} else {
82+
console.log(`${LOG_PREFIX} Installing extension ${prePackagedExtensionInfo.identifier}`);
83+
}
84+
try {
85+
if (!prePackagedExtensionInfo.path) {
86+
throw new Error(`Extension path missing for ${prePackagedExtensionInfo.identifier}`);
87+
}
88+
89+
const targetPath = path.join(PERSISTENT_VOLUME_EXTENSIONS_DIR, path.basename(prePackagedExtensionInfo.path));
90+
91+
// Remove existing symlink or directory if it exists
92+
try {
93+
console.log(`${LOG_PREFIX} Removing existing folder ${targetPath}`);
94+
await fs.unlink(targetPath);
95+
} catch (error) {
96+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
97+
console.error(`${LOG_PREFIX} Error removing existing extension:`, error);
98+
throw error;
99+
}
100+
// if file already doesn't exist then keep going
101+
}
102+
103+
// Create new symlink
104+
try {
105+
console.log(`${LOG_PREFIX} Adding extension to persistent volume directory`);
106+
await fs.symlink(prePackagedExtensionInfo.path, targetPath, 'dir');
107+
} catch (error) {
108+
console.error(`${LOG_PREFIX} Error adding extension to persistent volume directory:`, error);
109+
throw error;
110+
}
111+
112+
// Handle .obsolete file
113+
const OBSOLETE_FILE = path.join(PERSISTENT_VOLUME_EXTENSIONS_DIR, '.obsolete');
114+
let obsoleteData: Record<string, boolean> = {};
115+
116+
try {
117+
const obsoleteContent = await fs.readFile(OBSOLETE_FILE, 'utf-8');
118+
console.log(`${LOG_PREFIX} .obsolete file found`);
119+
obsoleteData = JSON.parse(obsoleteContent);
120+
} catch (error) {
121+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
122+
console.log(`${LOG_PREFIX} .obsolete file not found. Creating a new one.`);
123+
} else {
124+
console.warn(`${LOG_PREFIX} Error reading .obsolete file:`, error);
125+
// Backup malformed file
126+
const backupPath = `${OBSOLETE_FILE}.bak`;
127+
await fs.rename(OBSOLETE_FILE, backupPath);
128+
console.log(`${LOG_PREFIX} Backed up malformed .obsolete file to ${backupPath}`);
129+
}
130+
}
131+
132+
if (installedExtensionInfo?.path) {
133+
const obsoleteBasename = path.basename(installedExtensionInfo.path);
134+
obsoleteData[obsoleteBasename] = true;
135+
}
136+
const obsoleteBasenamePrepackaged = path.basename(prePackagedExtensionInfo.path);
137+
obsoleteData[obsoleteBasenamePrepackaged] = false;
138+
139+
try {
140+
console.log(`${LOG_PREFIX} Writing to .obsolete file.`);
141+
await fs.writeFile(OBSOLETE_FILE, JSON.stringify(obsoleteData, null, 2));
142+
} catch (error) {
143+
console.error(`${LOG_PREFIX} Error writing .obsolete file:`, error);
144+
throw error;
145+
}
146+
147+
console.log(`${LOG_PREFIX} Installed ${prePackagedExtensionInfo.identifier}`);
148+
} catch (error) {
149+
vscode.window.showErrorMessage(`Could not install extension ${prePackagedExtensionInfo.identifier}`);
150+
console.error(`${LOG_PREFIX} ${error}`);
151+
}
152+
}

0 commit comments

Comments
 (0)