From c5812340717dd52c7a4868ed65deae42e2b8758e Mon Sep 17 00:00:00 2001 From: Daniel Pilgrim-Minaya Date: Tue, 16 Jun 2026 19:47:39 +0000 Subject: [PATCH] fix(extensions-sync): Auto-repair dangling symlinks on SMD image upgrade Add repairDanglingSymlinks() to detect and fix broken symlinks in the persistent volume when a CodeEditor space restarts with a newer SMD image. Runs before existing sync logic so subsequent reads see clean state. Respects .obsolete file to avoid re-installing user-uninstalled extensions. --- patches/sagemaker-extensions-sync.patch | 93 ++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 3 deletions(-) diff --git a/patches/sagemaker-extensions-sync.patch b/patches/sagemaker-extensions-sync.patch index 2d6e6315b..d3e895d8a 100644 --- a/patches/sagemaker-extensions-sync.patch +++ b/patches/sagemaker-extensions-sync.patch @@ -174,7 +174,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,100 @@ +@@ -0,0 +1,105 @@ +import * as process from "process"; +import * as vscode from 'vscode'; + @@ -189,7 +189,8 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/ext + getExtensionsFromDirectory, + getInstalledExtensions, + installExtension, -+ refreshExtensionsMetadata } from "./utils" ++ refreshExtensionsMetadata, ++ repairDanglingSymlinks } from "./utils" + +export async function activate() { + @@ -199,6 +200,9 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/ext + return; + } + ++ // Auto-repair dangling symlinks from image upgrades before reading state ++ await repairDanglingSymlinks(); ++ + // 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()); @@ -304,7 +308,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,152 @@ +@@ -0,0 +1,236 @@ +import * as fs from "fs/promises"; +import * as path from "path"; +import * as vscode from 'vscode'; @@ -313,6 +317,7 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/uti + +import { + ExtensionInfo, ++ IMAGE_EXTENSIONS_DIR, + LOG_PREFIX, + PERSISTENT_VOLUME_EXTENSIONS_DIR, +} from "./constants" @@ -457,4 +462,86 @@ Index: sagemaker-code-editor/vscode/extensions/sagemaker-extensions-sync/src/uti + console.error(`${LOG_PREFIX} ${error}`); + } +} ++ ++function stripVersion(dirname: string): string | null { ++ const match = dirname.match(/^(.+)-\d+\.\d+\.\d+.*$/); ++ return match ? match[1] : null; ++} ++ ++export async function repairDanglingSymlinks(): Promise { ++ let pvItems: string[]; ++ try { ++ pvItems = await fs.readdir(PERSISTENT_VOLUME_EXTENSIONS_DIR); ++ } catch { ++ return; ++ } ++ ++ let imageItems: string[]; ++ try { ++ imageItems = await fs.readdir(IMAGE_EXTENSIONS_DIR); ++ } catch { ++ return; ++ } ++ ++ const OBSOLETE_FILE = path.join(PERSISTENT_VOLUME_EXTENSIONS_DIR, '.obsolete'); ++ let obsoleteData: Record = {}; ++ try { ++ obsoleteData = JSON.parse(await fs.readFile(OBSOLETE_FILE, 'utf-8')); ++ } catch { /* may not exist */ } ++ ++ let repaired = false; ++ ++ for (const item of pvItems) { ++ const itemPath = path.join(PERSISTENT_VOLUME_EXTENSIONS_DIR, item); ++ ++ let lstats; ++ try { ++ lstats = await fs.lstat(itemPath); ++ } catch { ++ continue; ++ } ++ if (!lstats.isSymbolicLink()) { ++ continue; ++ } ++ ++ try { ++ await fs.stat(itemPath); ++ continue; // valid symlink ++ } catch { /* dangling */ } ++ ++ if (obsoleteData[item] === true) { ++ await fs.unlink(itemPath); ++ continue; ++ } ++ ++ const oldIdentity = stripVersion(item); ++ let matched: string | undefined; ++ for (const imageItem of imageItems) { ++ if (oldIdentity && stripVersion(imageItem) === oldIdentity) { ++ matched = imageItem; ++ break; ++ } ++ } ++ ++ if (matched) { ++ await fs.unlink(itemPath); ++ const newLinkPath = path.join(PERSISTENT_VOLUME_EXTENSIONS_DIR, matched); ++ try { await fs.unlink(newLinkPath); } catch { /* ok */ } ++ await fs.symlink(path.join(IMAGE_EXTENSIONS_DIR, matched), newLinkPath, 'dir'); ++ obsoleteData[item] = true; ++ obsoleteData[matched] = false; ++ console.log(`${LOG_PREFIX} Auto-repaired: ${item} → ${matched}`); ++ repaired = true; ++ } else { ++ await fs.unlink(itemPath); ++ console.log(`${LOG_PREFIX} Cleaned dangling symlink: ${item}`); ++ repaired = true; ++ } ++ } ++ ++ if (repaired) { ++ await fs.writeFile(OBSOLETE_FILE, JSON.stringify(obsoleteData, null, 2)); ++ await refreshExtensionsMetadata(); ++ } ++} \ No newline at end of file