Skip to content

Commit 569a462

Browse files
committed
Add a race between login event and dialog promise when connecting and logged out
1 parent 786f8a3 commit 569a462

File tree

2 files changed

+73
-40
lines changed

2 files changed

+73
-40
lines changed

src/extension.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,15 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
309309
commands.viewLogs.bind(commands),
310310
);
311311

312+
const remote = new Remote(
313+
vscodeProposed,
314+
output,
315+
commands,
316+
ctx.extensionMode,
317+
pathResolver,
318+
cliManager,
319+
);
320+
312321
ctx.subscriptions.push(
313322
secretsManager.onDidChangeSessionToken(async () => {
314323
const token = await secretsManager.getSessionToken();
@@ -320,6 +329,8 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
320329
output.info("Logging in");
321330
// Should login the user directly if the URL+Token are valid
322331
await commands.login({ url, token });
332+
// Resolve any pending login detection promises
333+
remote.resolveLoginDetected();
323334
}
324335
}),
325336
);
@@ -334,14 +345,6 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
334345
// (this would require the user to uninstall the Coder extension and
335346
// reinstall after installing the remote SSH extension, which is annoying)
336347
if (remoteSSHExtension && vscodeProposed.env.remoteAuthority) {
337-
const remote = new Remote(
338-
vscodeProposed,
339-
output,
340-
commands,
341-
ctx.extensionMode,
342-
pathResolver,
343-
cliManager,
344-
);
345348
try {
346349
const details = await remote.setup(
347350
vscodeProposed.env.remoteAuthority,

src/remote.ts

Lines changed: 62 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ export interface RemoteDetails extends vscode.Disposable {
4444
}
4545

4646
export class Remote {
47+
private loginDetectedResolver: (() => void) | undefined;
48+
private loginDetectedPromise: Promise<void> = Promise.resolve();
49+
4750
public constructor(
4851
// We use the proposed API to get access to useCustom in dialogs.
4952
private readonly vscodeProposed: typeof vscode,
@@ -54,6 +57,27 @@ export class Remote {
5457
private readonly cliManager: CliManager,
5558
) {}
5659

60+
/**
61+
* Creates a new promise that will be resolved when login is detected in another window.
62+
* This should be called when starting a setup operation that might need login.
63+
*/
64+
private createLoginDetectionPromise(): void {
65+
this.loginDetectedPromise = new Promise<void>((resolve) => {
66+
this.loginDetectedResolver = resolve;
67+
});
68+
}
69+
70+
/**
71+
* Resolves the current login detection promise if one exists.
72+
* This should be called from the extension when login is detected.
73+
*/
74+
public resolveLoginDetected(): void {
75+
if (this.loginDetectedResolver) {
76+
this.loginDetectedResolver();
77+
this.loginDetectedResolver = undefined;
78+
}
79+
}
80+
5781
private async confirmStart(workspaceName: string): Promise<boolean> {
5882
const action = await this.vscodeProposed.window.showInformationMessage(
5983
`Unable to connect to the workspace ${workspaceName} because it is not running. Start the workspace?`,
@@ -217,34 +241,54 @@ export class Remote {
217241
// Migrate "session_token" file to "session", if needed.
218242
await this.migrateSessionToken(parts.label);
219243

244+
// Try to detect any login event that might happen after we read the current configs
245+
this.createLoginDetectionPromise();
220246
// Get the URL and token belonging to this host.
221247
const { url: baseUrlRaw, token } = await this.cliManager.readConfig(
222248
parts.label,
223249
);
224250

225-
// It could be that the cli config was deleted. If so, ask for the url.
226-
if (
227-
!baseUrlRaw ||
228-
(!token && needToken(vscode.workspace.getConfiguration()))
229-
) {
230-
const result = await this.vscodeProposed.window.showInformationMessage(
231-
"You are not logged in...",
251+
const showLoginDialog = async (message: string) => {
252+
const dialogPromise = this.vscodeProposed.window.showInformationMessage(
253+
message,
232254
{
233255
useCustom: true,
234256
modal: true,
235-
detail: `You must log in to access ${workspaceName}.`,
257+
detail: `You must log in to access ${workspaceName}. If you've already logged in, you may close this dialog.`,
236258
},
237259
"Log In",
238260
);
239-
if (!result) {
240-
// User declined to log in.
241-
await this.closeRemote();
261+
262+
// Race between dialog and login detection
263+
const result = await Promise.race([
264+
this.loginDetectedPromise.then(() => ({ type: "login" as const })),
265+
dialogPromise.then((userChoice) => ({
266+
type: "dialog" as const,
267+
userChoice,
268+
})),
269+
]);
270+
271+
if (result.type === "login") {
272+
return this.setup(remoteAuthority, firstConnect);
242273
} else {
243-
// Log in then try again.
244-
await this.commands.login({ url: baseUrlRaw, label: parts.label });
245-
await this.setup(remoteAuthority, firstConnect);
274+
if (!result.userChoice) {
275+
// User declined to log in.
276+
await this.closeRemote();
277+
return;
278+
} else {
279+
// Log in then try again.
280+
await this.commands.login({ url: baseUrlRaw, label: parts.label });
281+
return this.setup(remoteAuthority, firstConnect);
282+
}
246283
}
247-
return;
284+
};
285+
286+
// It could be that the cli config was deleted. If so, ask for the url.
287+
if (
288+
!baseUrlRaw ||
289+
(!token && needToken(vscode.workspace.getConfiguration()))
290+
) {
291+
return showLoginDialog("You are not logged in...");
248292
}
249293

250294
this.logger.info("Using deployment URL", baseUrlRaw);
@@ -315,6 +359,8 @@ export class Remote {
315359
// Next is to find the workspace from the URI scheme provided.
316360
let workspace: Workspace;
317361
try {
362+
// We could've logged out in the meantime
363+
this.createLoginDetectionPromise();
318364
this.logger.info(`Looking for workspace ${workspaceName}...`);
319365
workspace = await workspaceClient.getWorkspaceByOwnerAndName(
320366
parts.username,
@@ -348,23 +394,7 @@ export class Remote {
348394
return;
349395
}
350396
case 401: {
351-
const result =
352-
await this.vscodeProposed.window.showInformationMessage(
353-
"Your session expired...",
354-
{
355-
useCustom: true,
356-
modal: true,
357-
detail: `You must log in to access ${workspaceName}.`,
358-
},
359-
"Log In",
360-
);
361-
if (!result) {
362-
await this.closeRemote();
363-
} else {
364-
await this.commands.login({ url: baseUrlRaw, label: parts.label });
365-
await this.setup(remoteAuthority, firstConnect);
366-
}
367-
return;
397+
return showLoginDialog("Your session expired...");
368398
}
369399
default:
370400
throw error;

0 commit comments

Comments
 (0)