Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit648360a

Browse files
authored
Fix login/logout synchronization across multiple VS Code windows (#590)
Introduce ContextManager for centralized state management and use secrets to propagate authentication events between windows. Resolves race conditions in session token handling and ensures consistent authentication behavior across all open extension instances.Fixes#498
1 parent4600567 commit648360a

File tree

11 files changed

+336
-122
lines changed

11 files changed

+336
-122
lines changed

‎.eslintrc.json‎

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,6 @@
2323
"import/internal-regex":"^@/"
2424
},
2525
"overrides": [
26-
{
27-
"files": ["test/**/*.{ts,tsx}","**/*.{test,spec}.ts?(x)"],
28-
"settings": {
29-
"import/resolver": {
30-
"typescript": {
31-
// In tests, resolve using the test tsconfig
32-
"project":"test/tsconfig.json"
33-
}
34-
}
35-
}
36-
},
3726
{
3827
"files": ["*.ts"],
3928
"rules": {
@@ -46,9 +35,30 @@
4635
"prefer":"type-imports",
4736
"fixStyle":"inline-type-imports"
4837
}
38+
],
39+
"@typescript-eslint/switch-exhaustiveness-check": [
40+
"error",
41+
{"considerDefaultExhaustiveForUnions":true }
4942
]
5043
}
5144
},
45+
{
46+
"files": ["test/**/*.{ts,tsx}","**/*.{test,spec}.ts?(x)"],
47+
"settings": {
48+
"import/resolver": {
49+
"typescript": {
50+
// In tests, resolve using the test tsconfig
51+
"project":"test/tsconfig.json"
52+
}
53+
}
54+
}
55+
},
56+
{
57+
"files": ["src/core/contextManager.ts"],
58+
"rules": {
59+
"no-restricted-syntax":"off"
60+
}
61+
},
5262
{
5363
"extends": ["plugin:package-json/legacy-recommended"],
5464
"files": ["*.json"],
@@ -106,6 +116,13 @@
106116
"sublings_only":true
107117
}
108118
}
119+
],
120+
"no-restricted-syntax": [
121+
"error",
122+
{
123+
"selector":"CallExpression[callee.property.name='executeCommand'][arguments.0.value='setContext'][arguments.length>=3]",
124+
"message":"Do not use executeCommand('setContext', ...) directly. Use the ContextManager class instead."
125+
}
109126
]
110127
}
111128
}

‎src/commands.ts‎

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { CoderApi } from "./api/coderApi";
1212
import{needToken}from"./api/utils";
1313
import{typeCliManager}from"./core/cliManager";
1414
import{typeServiceContainer}from"./core/container";
15+
import{typeContextManager}from"./core/contextManager";
1516
import{typeMementoManager}from"./core/mementoManager";
1617
import{typePathResolver}from"./core/pathResolver";
1718
import{typeSecretsManager}from"./core/secretsManager";
@@ -32,6 +33,7 @@ export class Commands {
3233
privatereadonlymementoManager:MementoManager;
3334
privatereadonlysecretsManager:SecretsManager;
3435
privatereadonlycliManager:CliManager;
36+
privatereadonlycontextManager:ContextManager;
3537
// These will only be populated when actively connected to a workspace and are
3638
// used in commands. Because commands can be executed by the user, it is not
3739
// possible to pass in arguments, so we have to store the current workspace
@@ -53,6 +55,7 @@ export class Commands {
5355
this.mementoManager=serviceContainer.getMementoManager();
5456
this.secretsManager=serviceContainer.getSecretsManager();
5557
this.cliManager=serviceContainer.getCliManager();
58+
this.contextManager=serviceContainer.getContextManager();
5659
}
5760

5861
/**
@@ -179,31 +182,34 @@ export class Commands {
179182
}
180183

181184
/**
182-
* Log into the provided deployment.If the deployment URL is not specified,
185+
* Log into the provided deployment. If the deployment URL is not specified,
183186
* ask for it first with a menu showing recent URLs along with the default URL
184187
* and CODER_URL, if those are set.
185188
*/
186-
publicasynclogin(...args:string[]):Promise<void>{
187-
// Destructure would be nice but VS Code can pass undefined which errors.
188-
constinputUrl=args[0];
189-
constinputToken=args[1];
190-
constinputLabel=args[2];
191-
constisAutologin=
192-
typeofargs[3]==="undefined" ?false :Boolean(args[3]);
193-
194-
consturl=awaitthis.maybeAskUrl(inputUrl);
189+
publicasynclogin(args?:{
190+
url?:string;
191+
token?:string;
192+
label?:string;
193+
autoLogin?:boolean;
194+
}):Promise<void>{
195+
if(this.contextManager.get("coder.authenticated")){
196+
return;
197+
}
198+
this.logger.info("Logging in");
199+
200+
consturl=awaitthis.maybeAskUrl(args?.url);
195201
if(!url){
196202
return;// The user aborted.
197203
}
198204

199205
// It is possible that we are trying to log into an old-style host, in which
200206
// case we want to write with the provided blank label instead of generating
201207
// a host label.
202-
constlabel=
203-
typeofinputLabel==="undefined" ?toSafeHost(url) :inputLabel;
208+
constlabel=args?.label===undefined ?toSafeHost(url) :args.label;
204209

205210
// Try to get a token from the user, if we need one, and their user.
206-
constres=awaitthis.maybeAskToken(url,inputToken,isAutologin);
211+
constautoLogin=args?.autoLogin===true;
212+
constres=awaitthis.maybeAskToken(url,args?.token,autoLogin);
207213
if(!res){
208214
return;// The user aborted, or unable to auth.
209215
}
@@ -221,13 +227,9 @@ export class Commands {
221227
awaitthis.cliManager.configure(label,url,res.token);
222228

223229
// These contexts control various menu items and the sidebar.
224-
awaitvscode.commands.executeCommand(
225-
"setContext",
226-
"coder.authenticated",
227-
true,
228-
);
230+
this.contextManager.set("coder.authenticated",true);
229231
if(res.user.roles.find((role)=>role.name==="owner")){
230-
awaitvscode.commands.executeCommand("setContext","coder.isOwner",true);
232+
this.contextManager.set("coder.isOwner",true);
231233
}
232234

233235
vscode.window
@@ -245,6 +247,7 @@ export class Commands {
245247
}
246248
});
247249

250+
awaitthis.secretsManager.triggerLoginStateChange("login");
248251
// Fetch workspaces for the new deployment.
249252
vscode.commands.executeCommand("coder.refreshWorkspaces");
250253
}
@@ -257,19 +260,21 @@ export class Commands {
257260
*/
258261
privateasyncmaybeAskToken(
259262
url:string,
260-
token:string,
261-
isAutologin:boolean,
263+
token:string|undefined,
264+
isAutoLogin:boolean,
262265
):Promise<{user:User;token:string}|null>{
263266
constclient=CoderApi.create(url,token,this.logger);
264-
if(!needToken(vscode.workspace.getConfiguration())){
267+
constneedsToken=needToken(vscode.workspace.getConfiguration());
268+
if(!needsToken||token){
265269
try{
266270
constuser=awaitclient.getAuthenticatedUser();
267271
// For non-token auth, we write a blank token since the `vscodessh`
268272
// command currently always requires a token file.
269-
return{token:"", user};
273+
// For token auth, we have valid access so we can just return the user here
274+
return{token:needsToken&&token ?token :"", user};
270275
}catch(err){
271276
constmessage=getErrorMessage(err,"no response from the server");
272-
if(isAutologin){
277+
if(isAutoLogin){
273278
this.logger.warn("Failed to log in to Coder server:",message);
274279
}else{
275280
this.vscodeProposed.window.showErrorMessage(
@@ -301,6 +306,9 @@ export class Commands {
301306
value:token||(awaitthis.secretsManager.getSessionToken()),
302307
ignoreFocusOut:true,
303308
validateInput:async(value)=>{
309+
if(!value){
310+
returnnull;
311+
}
304312
client.setSessionToken(value);
305313
try{
306314
user=awaitclient.getAuthenticatedUser();
@@ -369,7 +377,14 @@ export class Commands {
369377
// Sanity check; command should not be available if no url.
370378
thrownewError("You are not logged in");
371379
}
380+
awaitthis.forceLogout();
381+
}
372382

383+
publicasyncforceLogout():Promise<void>{
384+
if(!this.contextManager.get("coder.authenticated")){
385+
return;
386+
}
387+
this.logger.info("Logging out");
373388
// Clear from the REST client. An empty url will indicate to other parts of
374389
// the code that we are logged out.
375390
this.restClient.setHost("");
@@ -379,19 +394,16 @@ export class Commands {
379394
awaitthis.mementoManager.setUrl(undefined);
380395
awaitthis.secretsManager.setSessionToken(undefined);
381396

382-
awaitvscode.commands.executeCommand(
383-
"setContext",
384-
"coder.authenticated",
385-
false,
386-
);
397+
this.contextManager.set("coder.authenticated",false);
387398
vscode.window
388399
.showInformationMessage("You've been logged out of Coder!","Login")
389400
.then((action)=>{
390401
if(action==="Login"){
391-
vscode.commands.executeCommand("coder.login");
402+
this.login();
392403
}
393404
});
394405

406+
awaitthis.secretsManager.triggerLoginStateChange("logout");
395407
// This will result in clearing the workspace list.
396408
vscode.commands.executeCommand("coder.refreshWorkspaces");
397409
}

‎src/core/container.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as vscode from "vscode";
33
import{typeLogger}from"../logging/logger";
44

55
import{CliManager}from"./cliManager";
6+
import{ContextManager}from"./contextManager";
67
import{MementoManager}from"./mementoManager";
78
import{PathResolver}from"./pathResolver";
89
import{SecretsManager}from"./secretsManager";
@@ -17,6 +18,7 @@ export class ServiceContainer implements vscode.Disposable {
1718
privatereadonlymementoManager:MementoManager;
1819
privatereadonlysecretsManager:SecretsManager;
1920
privatereadonlycliManager:CliManager;
21+
privatereadonlycontextManager:ContextManager;
2022

2123
constructor(
2224
context:vscode.ExtensionContext,
@@ -34,6 +36,7 @@ export class ServiceContainer implements vscode.Disposable {
3436
this.logger,
3537
this.pathResolver,
3638
);
39+
this.contextManager=newContextManager();
3740
}
3841

3942
getVsCodeProposed():typeofvscode{
@@ -60,10 +63,15 @@ export class ServiceContainer implements vscode.Disposable {
6063
returnthis.cliManager;
6164
}
6265

66+
getContextManager():ContextManager{
67+
returnthis.contextManager;
68+
}
69+
6370
/**
6471
* Dispose of all services and clean up resources.
6572
*/
6673
dispose():void{
74+
this.contextManager.dispose();
6775
this.logger.dispose();
6876
}
6977
}

‎src/core/contextManager.ts‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import*asvscodefrom"vscode";
2+
3+
constCONTEXT_DEFAULTS={
4+
"coder.authenticated":false,
5+
"coder.isOwner":false,
6+
"coder.loaded":false,
7+
"coder.workspace.updatable":false,
8+
}asconst;
9+
10+
typeCoderContext=keyoftypeofCONTEXT_DEFAULTS;
11+
12+
exportclassContextManagerimplementsvscode.Disposable{
13+
privatereadonlycontext=newMap<CoderContext,boolean>();
14+
15+
publicconstructor(){
16+
(Object.keys(CONTEXT_DEFAULTS)asCoderContext[]).forEach((key)=>{
17+
this.set(key,CONTEXT_DEFAULTS[key]);
18+
});
19+
}
20+
21+
publicset(key:CoderContext,value:boolean):void{
22+
this.context.set(key,value);
23+
vscode.commands.executeCommand("setContext",key,value);
24+
}
25+
26+
publicget(key:CoderContext):boolean{
27+
returnthis.context.get(key)??CONTEXT_DEFAULTS[key];
28+
}
29+
30+
publicdispose(){
31+
this.context.clear();
32+
}
33+
}

‎src/core/secretsManager.ts‎

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
1-
importtype{SecretStorage}from"vscode";
1+
importtype{SecretStorage,Disposable}from"vscode";
2+
3+
constSESSION_TOKEN_KEY="sessionToken";
4+
5+
constLOGIN_STATE_KEY="loginState";
6+
7+
exportenumAuthAction{
8+
LOGIN,
9+
LOGOUT,
10+
INVALID,
11+
}
212

313
exportclassSecretsManager{
414
constructor(privatereadonlysecrets:SecretStorage){}
@@ -8,9 +18,9 @@ export class SecretsManager {
818
*/
919
publicasyncsetSessionToken(sessionToken?:string):Promise<void>{
1020
if(!sessionToken){
11-
awaitthis.secrets.delete("sessionToken");
21+
awaitthis.secrets.delete(SESSION_TOKEN_KEY);
1222
}else{
13-
awaitthis.secrets.store("sessionToken",sessionToken);
23+
awaitthis.secrets.store(SESSION_TOKEN_KEY,sessionToken);
1424
}
1525
}
1626

@@ -19,11 +29,45 @@ export class SecretsManager {
1929
*/
2030
publicasyncgetSessionToken():Promise<string|undefined>{
2131
try{
22-
returnawaitthis.secrets.get("sessionToken");
32+
returnawaitthis.secrets.get(SESSION_TOKEN_KEY);
2333
}catch{
2434
// The VS Code session store has become corrupt before, and
2535
// will fail to get the session token...
2636
returnundefined;
2737
}
2838
}
39+
40+
/**
41+
* Triggers a login/logout event that propagates across all VS Code windows.
42+
* Uses the secrets storage onDidChange event as a cross-window communication mechanism.
43+
* Appends a timestamp to ensure the value always changes, guaranteeing the event fires.
44+
*/
45+
publicasynctriggerLoginStateChange(
46+
action:"login"|"logout",
47+
):Promise<void>{
48+
constdate=newDate().toISOString();
49+
awaitthis.secrets.store(LOGIN_STATE_KEY,`${action}-${date}`);
50+
}
51+
52+
/**
53+
* Listens for login/logout events from any VS Code window.
54+
* The secrets storage onDidChange event fires across all windows, enabling cross-window sync.
55+
*/
56+
publiconDidChangeLoginState(
57+
listener:(state:AuthAction)=>Promise<void>,
58+
):Disposable{
59+
returnthis.secrets.onDidChange(async(e)=>{
60+
if(e.key===LOGIN_STATE_KEY){
61+
conststate=awaitthis.secrets.get(LOGIN_STATE_KEY);
62+
if(state?.startsWith("login")){
63+
listener(AuthAction.LOGIN);
64+
}elseif(state?.startsWith("logout")){
65+
listener(AuthAction.LOGOUT);
66+
}else{
67+
// Secret was deleted or is invalid
68+
listener(AuthAction.INVALID);
69+
}
70+
}
71+
});
72+
}
2973
}

‎src/error.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ export class CertificateError extends Error {
6464
returnnewCertificateError(err.message,X509_ERR.UNTRUSTED_LEAF);
6565
caseX509_ERR_CODE.SELF_SIGNED_CERT_IN_CHAIN:
6666
returnnewCertificateError(err.message,X509_ERR.UNTRUSTED_CHAIN);
67+
caseundefined:
68+
break;
6769
}
6870
}
6971
returnerr;
@@ -154,6 +156,7 @@ export class CertificateError extends Error {
154156
);
155157
switch(val){
156158
caseCertificateError.ActionOK:
159+
caseundefined:
157160
return;
158161
caseCertificateError.ActionAllowInsecure:
159162
awaitthis.allowInsecure();

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp