Skip to content

Commit 1937081

Browse files
Merge branch 'master' into fix/SSH-host
2 parents 078ca7a + e369ff3 commit 1937081

File tree

18 files changed

+677
-74
lines changed

18 files changed

+677
-74
lines changed

packages/core/src/awsService/sagemaker/commands.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,16 @@ export async function deeplinkConnect(
115115
wsUrl: string,
116116
token: string,
117117
domain: string,
118-
appType?: string
118+
appType?: string,
119+
isSMUS: boolean = false
119120
) {
120121
getLogger().debug(
121-
`sm:deeplinkConnect: connectionIdentifier: ${connectionIdentifier} session: ${session} wsUrl: ${wsUrl} token: ${token}`
122+
'sm:deeplinkConnect: connectionIdentifier: %s session: %s wsUrl: %s token: %s isSMUS: %s',
123+
connectionIdentifier,
124+
session,
125+
wsUrl,
126+
token,
127+
isSMUS
122128
)
123129

124130
if (isRemoteWorkspace()) {
@@ -134,7 +140,7 @@ export async function deeplinkConnect(
134140
connectionIdentifier,
135141
ctx.extensionContext,
136142
'sm_dl',
137-
false /* isSMUS */,
143+
isSMUS,
138144
undefined /* node */,
139145
session,
140146
wsUrl,
@@ -152,7 +158,10 @@ export async function deeplinkConnect(
152158
)
153159
} catch (err: any) {
154160
getLogger().error(
155-
`sm:OpenRemoteConnect: Unable to connect to target space with arn: ${connectionIdentifier} error: ${err}`
161+
'sm:OpenRemoteConnect: Unable to connect to target space with arn: %s error: %s isSMUS: %s',
162+
connectionIdentifier,
163+
err,
164+
isSMUS
156165
)
157166

158167
if (![RemoteSessionError.MissingExtension, RemoteSessionError.ExtensionVersionTooLow].includes(err.code)) {
@@ -357,7 +366,8 @@ async function handleRunningSpaceWithDisabledAccess(
357366
await client.waitForAppInService(
358367
node.spaceApp.DomainId!,
359368
spaceName,
360-
node.spaceApp.SpaceSettingsSummary!.AppType!
369+
node.spaceApp.SpaceSettingsSummary!.AppType!,
370+
progress
361371
)
362372
await tryRemoteConnection(node, ctx, progress)
363373
} catch (err: any) {
@@ -401,7 +411,8 @@ async function handleStoppedSpace(
401411
await client.waitForAppInService(
402412
node.spaceApp.DomainId!,
403413
spaceName,
404-
node.spaceApp.SpaceSettingsSummary!.AppType!
414+
node.spaceApp.SpaceSettingsSummary!.AppType!,
415+
progress
405416
)
406417
await tryRemoteConnection(node, ctx, progress)
407418
}

packages/core/src/awsService/sagemaker/constants.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,10 @@ export const SshConfigRemovalFailedMessage = (configHostName: string) =>
6161

6262
export const SshConfigUpdateFailedMessage = (configPath: string, configHostName: string) =>
6363
`Failed to update SSH config section. Fix your ${configPath} file manually or remove the outdated ${configHostName} section.`
64+
export const SmusDeeplinkSessionExpiredError = {
65+
title: 'Session Disconnected',
66+
message:
67+
'Your SageMaker Unified Studio session has been disconnected. Select a local (non-remote) VS Code window and use the SageMaker Unified Studio portal to connect again.',
68+
code: 'SMUS_SESSION_DISCONNECTED',
69+
shortMessage: 'Session disconnected, re-connect from SageMaker Unified Studio portal.',
70+
} as const

packages/core/src/awsService/sagemaker/credentialMapping.ts

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -90,41 +90,51 @@ export async function persistSmusProjectCreds(spaceArn: string, node: SagemakerU
9090
* @param session - SSM session ID.
9191
* @param wsUrl - SSM WebSocket URL.
9292
* @param token - Bearer token for the session.
93+
* @param appType - Application type (e.g., 'jupyterlab', 'codeeditor').
94+
* @param isSMUS - If true, skip refreshUrl construction (SMUS connections cannot refresh).
9395
*/
9496
export async function persistSSMConnection(
9597
spaceArn: string,
9698
domain: string,
9799
session?: string,
98100
wsUrl?: string,
99101
token?: string,
100-
appType?: string
102+
appType?: string,
103+
isSMUS?: boolean
101104
): Promise<void> {
102-
const { region } = parseArn(spaceArn)
103-
const endpoint = DevSettings.instance.get('endpoints', {})['sagemaker'] ?? ''
105+
let refreshUrl: string | undefined
104106

105-
let appSubDomain = 'jupyterlab'
106-
if (appType && appType.toLowerCase() === 'codeeditor') {
107-
appSubDomain = 'code-editor'
108-
}
107+
if (!isSMUS) {
108+
// Construct refreshUrl for SageMaker AI connections
109+
const { region } = parseArn(spaceArn)
110+
const endpoint = DevSettings.instance.get('endpoints', {})['sagemaker'] ?? ''
109111

110-
let envSubdomain: string
112+
let appSubDomain = 'jupyterlab'
113+
if (appType && appType.toLowerCase() === 'codeeditor') {
114+
appSubDomain = 'code-editor'
115+
}
111116

112-
if (endpoint.includes('beta')) {
113-
envSubdomain = 'devo'
114-
} else if (endpoint.includes('gamma')) {
115-
envSubdomain = 'loadtest'
116-
} else {
117-
envSubdomain = 'studio'
118-
}
117+
let envSubdomain: string
119118

120-
// Use the standard AWS domain for 'studio' (prod).
121-
// For non-prod environments, use the obfuscated domain 'asfiovnxocqpcry.com'.
122-
const baseDomain =
123-
envSubdomain === 'studio'
124-
? `studio.${region}.sagemaker.aws`
125-
: `${envSubdomain}.studio.${region}.asfiovnxocqpcry.com`
119+
if (endpoint.includes('beta')) {
120+
envSubdomain = 'devo'
121+
} else if (endpoint.includes('gamma')) {
122+
envSubdomain = 'loadtest'
123+
} else {
124+
envSubdomain = 'studio'
125+
}
126+
127+
// Use the standard AWS domain for 'studio' (prod).
128+
// For non-prod environments, use the obfuscated domain 'asfiovnxocqpcry.com'.
129+
const baseDomain =
130+
envSubdomain === 'studio'
131+
? `studio.${region}.sagemaker.aws`
132+
: `${envSubdomain}.studio.${region}.asfiovnxocqpcry.com`
133+
134+
refreshUrl = `https://studio-${domain}.${baseDomain}/${appSubDomain}`
135+
}
136+
// For SMUS connections, refreshUrl remains undefined
126137

127-
const refreshUrl = `https://studio-${domain}.${baseDomain}/${appSubDomain}`
128138
await setSpaceCredentials(spaceArn, refreshUrl, {
129139
sessionId: session ?? '-',
130140
url: wsUrl ?? '-',
@@ -179,12 +189,12 @@ export async function setSmusSpaceSsoProfile(spaceArn: string, projectId: string
179189
* Stores SSM connection information for a given space, typically from a deep link session.
180190
* This initializes the request as 'fresh' and includes a refresh URL if provided.
181191
* @param spaceArn - The arn of the SageMaker space.
182-
* @param refreshUrl - URL to use for refreshing session tokens.
192+
* @param refreshUrl - URL to use for refreshing session tokens (undefined for SMUS connections).
183193
* @param credentials - The session information used to initiate the connection.
184194
*/
185195
export async function setSpaceCredentials(
186196
spaceArn: string,
187-
refreshUrl: string,
197+
refreshUrl: string | undefined,
188198
credentials: SsmConnectionInfo
189199
): Promise<void> {
190200
const data = await loadMappings()

packages/core/src/awsService/sagemaker/detached-server/routes/getSessionAsync.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { IncomingMessage, ServerResponse } from 'http'
99
import url from 'url'
1010
import { SessionStore } from '../sessionStore'
1111
import { open, parseArn, readServerInfo } from '../utils'
12+
import { openErrorPage } from '../errorPage'
13+
import { SmusDeeplinkSessionExpiredError } from '../../constants'
1214

1315
export async function handleGetSessionAsync(req: IncomingMessage, res: ServerResponse): Promise<void> {
1416
const parsedUrl = url.parse(req.url || '', true)
@@ -46,8 +48,34 @@ export async function handleGetSessionAsync(req: IncomingMessage, res: ServerRes
4648
res.end()
4749
return
4850
} else if (status === 'not-started') {
49-
const serverInfo = await readServerInfo()
5051
const refreshUrl = await store.getRefreshUrl(connectionIdentifier)
52+
53+
// Check if this is a SMUS connection (no refreshUrl available)
54+
if (refreshUrl === undefined) {
55+
console.log(`SMUS session expired for connection: ${connectionIdentifier}`)
56+
57+
// Clean up the expired connection entry
58+
try {
59+
await store.cleanupExpiredConnection(connectionIdentifier)
60+
console.log(`Cleaned up expired connection: ${connectionIdentifier}`)
61+
} catch (cleanupErr) {
62+
console.error(`Failed to cleanup expired connection: ${cleanupErr}`)
63+
// Continue with error response even if cleanup fails
64+
}
65+
66+
await openErrorPage(SmusDeeplinkSessionExpiredError.title, SmusDeeplinkSessionExpiredError.message)
67+
res.writeHead(400, { 'Content-Type': 'application/json' })
68+
res.end(
69+
JSON.stringify({
70+
error: SmusDeeplinkSessionExpiredError.code,
71+
message: SmusDeeplinkSessionExpiredError.shortMessage,
72+
})
73+
)
74+
return
75+
}
76+
77+
// Continue with existing SageMaker AI refresh flow
78+
const serverInfo = await readServerInfo()
5179
const { spaceName } = parseArn(connectionIdentifier)
5280

5381
const url = `${refreshUrl}/${encodeURIComponent(spaceName)}?remote_access_token_refresh=true&reconnect_identifier=${encodeURIComponent(

packages/core/src/awsService/sagemaker/detached-server/sessionStore.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { readMapping, writeMapping } from './utils'
99
export type SessionStatus = 'pending' | 'fresh' | 'consumed' | 'not-started'
1010

1111
export class SessionStore {
12-
async getRefreshUrl(connectionId: string) {
12+
async getRefreshUrl(connectionId: string): Promise<string | undefined> {
1313
const mapping = await readMapping()
1414

1515
if (!mapping.deepLink) {
@@ -21,10 +21,6 @@ export class SessionStore {
2121
throw new Error(`No mapping found for connectionId: "${connectionId}"`)
2222
}
2323

24-
if (!entry.refreshUrl) {
25-
throw new Error(`No refreshUrl found for connectionId: "${connectionId}"`)
26-
}
27-
2824
return entry.refreshUrl
2925
}
3026

@@ -113,6 +109,20 @@ export class SessionStore {
113109
await writeMapping(mapping)
114110
}
115111

112+
async cleanupExpiredConnection(connectionId: string) {
113+
const mapping = await readMapping()
114+
115+
if (!mapping.deepLink) {
116+
throw new Error('No deepLink mapping found')
117+
}
118+
119+
// Remove the entire connection entry for the expired space
120+
if (mapping.deepLink[connectionId]) {
121+
delete mapping.deepLink[connectionId]
122+
await writeMapping(mapping)
123+
}
124+
}
125+
116126
async setSession(connectionId: string, requestId: string, ssmConnectionInfo: SsmConnectionInfo) {
117127
const mapping = await readMapping()
118128

packages/core/src/awsService/sagemaker/model.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export async function prepareDevEnvConnection(
8484
await persistSmusProjectCreds(spaceArn, node as SagemakerUnifiedStudioSpaceNode)
8585
}
8686
} else if (connectionType === 'sm_dl') {
87-
await persistSSMConnection(spaceArn, domain ?? '', session, wsUrl, token, appType)
87+
await persistSSMConnection(spaceArn, domain ?? '', session, wsUrl, token, appType, isSMUS)
8888
}
8989

9090
await startLocalServer(ctx)

packages/core/src/extensionNode.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ export async function activate(context: vscode.ExtensionContext) {
199199
await handleAmazonQInstall()
200200
}
201201

202-
await activateSageMakerUnifiedStudio(context)
202+
await activateSageMakerUnifiedStudio(extContext)
203203

204204
await activateApplicationComposer(context)
205205
await activateThreatComposerEditor(context)

packages/core/src/sagemakerunifiedstudio/activation.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,25 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import * as vscode from 'vscode'
76
import { activate as activateConnectionMagicsSelector } from './connectionMagicsSelector/activation'
87
import { activate as activateExplorer } from './explorer/activation'
98
import { isSageMaker } from '../shared/extensionUtilities'
109
import { initializeResourceMetadata } from './shared/utils/resourceMetadataUtils'
1110
import { setContext } from '../shared/vscode/setContext'
1211
import { SmusUtils } from './shared/smusUtils'
12+
import * as smusUriHandlers from './uriHandlers'
13+
import { ExtContext } from '../shared/extensions'
1314

14-
export async function activate(extensionContext: vscode.ExtensionContext): Promise<void> {
15+
export async function activate(ctx: ExtContext): Promise<void> {
1516
// Only run when environment is a SageMaker Unified Studio space
1617
if (isSageMaker('SMUS') || isSageMaker('SMUS-SPACE-REMOTE-ACCESS')) {
1718
await initializeResourceMetadata()
1819
// Setting context before any getContext calls to avoid potential race conditions.
1920
await setContext('aws.smus.inSmusSpaceEnvironment', SmusUtils.isInSmusSpaceEnvironment())
20-
await activateConnectionMagicsSelector(extensionContext)
21+
await activateConnectionMagicsSelector(ctx.extensionContext)
2122
}
22-
await activateExplorer(extensionContext)
23+
await activateExplorer(ctx.extensionContext)
24+
25+
// Register SMUS URI handler for deeplink connections
26+
ctx.extensionContext.subscriptions.push(smusUriHandlers.register(ctx))
2327
}

0 commit comments

Comments
 (0)