Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 28 additions & 12 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,31 @@ const DEFAULT_SERVER_VERSION = "2016.2.0";
import * as Atelier from "./atelier";
import { isfsConfig } from "../utils/FileProviderUtil";

// Map of the authRequest promises for each username@host:port/pathPrefix target to avoid concurrency issues
// Map of the authRequest promises for each `username@http(s)://host:port/pathPrefix` target to avoid concurrency issues
const authRequestMap = new Map<string, Promise<any>>();

/** Map of `username@host:port/pathPrefix` to cookies */
const cookiesMap = new Map<string, string[]>();
/** Map of `username@http(s)://host:port/pathPrefix` to cookies */
export const cookiesMap = new Map<string, string[]>();

/** Log out of CSP sessions specified in the `sessions` array, or all known sessions if no argument was passed. */
export async function logoutOfSessions(sessions?: string[]): Promise<void> {
const httpsAgent = new httpsModule.Agent({
rejectUnauthorized: vscode.workspace.getConfiguration("http").get("proxyStrictSSL"),
});
return Promise.allSettled(
(sessions ?? Array.from(cookiesMap.keys())).map((session) => {
const cookie = cookiesMap.get(session);
if (!cookie) return;
const url = session.slice(session.indexOf("@") + 1);
return axios.head(`${url}/api/atelier/?CacheLogout=end`, {
headers: {
Cookie: cookie,
},
httpsAgent,
});
})
).then(() => {}); // Returned object isn't needed
}

interface ConnectionSettings {
serverName: string;
Expand Down Expand Up @@ -175,10 +195,6 @@ export class AtelierAPI {
return cookiesMap.get(this.mapKey()) ?? [];
}

public clearCookies(): void {
cookiesMap.delete(this.mapKey());
}

public xdebugUrl(): string {
const { host, https, port, apiVersion, pathPrefix } = this.config;
const proto = https ? "wss" : "ws";
Expand Down Expand Up @@ -208,13 +224,13 @@ export class AtelierAPI {
}

/** Return the key for getting values from connection-specific Maps for this connection */
private mapKey(): string {
const { host, port, username } = this.config;
public mapKey(): string {
const { host, https, port, username } = this.config;
let pathPrefix = this._config.pathPrefix || "";
if (pathPrefix.length && !pathPrefix.startsWith("/")) {
pathPrefix = "/" + pathPrefix;
}
return `${username}@${host}:${port}${pathPrefix}`;
return `${username}@http${https ? "s" : ""}://${host}:${port}${pathPrefix}`;
}

private setConnection(workspaceFolderName: string, namespace?: string): void {
Expand Down Expand Up @@ -307,7 +323,7 @@ export class AtelierAPI {
return serverName && serverName !== "" ? serverName : `${host}:${port}${pathPrefix}`;
}

public async request(
private async request(
minVersion: number,
method: string,
path?: string,
Expand Down Expand Up @@ -457,7 +473,7 @@ export class AtelierAPI {
}
throw { statusCode: response.status, message: response.statusText };
}
await this.updateCookies(response.headers["set-cookie"] || []);
this.updateCookies(response.headers["set-cookie"] || []);
if (method === "HEAD") {
if (!originalPath) {
authRequestMap.delete(mapKey);
Expand Down
101 changes: 40 additions & 61 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ import { ObjectScriptRoutineSymbolProvider } from "./providers/ObjectScriptRouti
import { ObjectScriptCodeLensProvider } from "./providers/ObjectScriptCodeLensProvider";
import { XmlContentProvider } from "./providers/XmlContentProvider";

import { AtelierAPI } from "./api";
import { AtelierAPI, cookiesMap, logoutOfSessions } from "./api";
import { ObjectScriptDebugAdapterDescriptorFactory } from "./debug/debugAdapterFactory";
import { ObjectScriptConfigurationProvider } from "./debug/debugConfProvider";
import { ProjectsExplorerProvider } from "./explorer/projectsExplorer";
Expand Down Expand Up @@ -340,20 +340,16 @@ export function getResolvedConnectionSpec(key: string, dflt: any): any {
/** The `api.serverId`s of all servers that are known to be inactive */
export const inactiveServerIds: Set<string> = new Set();

export async function checkConnection(
clearCookies = false,
uri?: vscode.Uri,
triggerRefreshes?: boolean
): Promise<void> {
export async function checkConnection(clearState = false, uri?: vscode.Uri, triggerRefreshes?: boolean): Promise<void> {
// Do nothing if already checking the connection
if (checkingConnection) {
return;
}

const { apiTarget, configName } = connectionTarget(uri);
const wsKey = configName.toLowerCase();
if (clearCookies) {
/// clean-up cached values
if (clearState) {
// clean-up cached values
await workspaceState.update(wsKey + ":host", undefined);
await workspaceState.update(wsKey + ":port", undefined);
await workspaceState.update(wsKey + ":superserverPort", undefined);
Expand Down Expand Up @@ -422,10 +418,6 @@ export async function checkConnection(
}
}

if (clearCookies) {
api.clearCookies();
}

// Before recreating the api object (in case something has updated connection details since we last fetched them?)
// if this is an external server, remove it from the inactive list because its presence there would block the reconnect that we want to be attempting
if (api.externalServer) {
Expand Down Expand Up @@ -1521,38 +1513,44 @@ export async function activate(context: vscode.ExtensionContext): Promise<any> {
supportsMultipleEditorsPerDocument: false,
}),
vscode.workspace.onDidChangeConfiguration(async ({ affectsConfiguration }) => {
if (affectsConfiguration("objectscript.conn") || affectsConfiguration("intersystems.servers")) {
if (affectsConfiguration("intersystems.servers")) {
// Gather the server names previously resolved
const resolvedServers: string[] = [];
resolvedConnSpecs.forEach((v, k) => resolvedServers.push(k));
// Clear the cache
resolvedConnSpecs.clear();
// Resolve them again, sequentially in case user needs to be prompted for credentials
for await (const serverName of resolvedServers) {
await resolveConnectionSpec(serverName);
}
}
Comment on lines -1524 to -1535

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First, thank you for this fix! It indeed fixes the problem I reported in #1805.

--

But I noticed one new problem: changes to a server in intersystems.servers no longer take effect until I reload the window.

Reproducable by:

  1. Connect normally. Set "objectscript.outputRESTTraffic": true to see the requests in the "ObjectScript" output.
  2. In intersystems.servers, change the webServer.port of that server to a wrong/closed port and save.
  3. The connection stays connected, and the output still shows requests going to the old port.
  4. Run "Developer: Reload Window". Only now the new port is used, and the connection fails (because the port is wrong).

On 3.8.2 the same change takes effect immediately, without a reload.

--

It looks like the old code re-read the server settings when intersystems.servers changed, and that part was removed in this PR. So the extension seems to keep using the old (cached) connection details until a reload. Maybe the fix needs to re-read / refresh the server spec for the changed servers again.

// Check connections sequentially for each workspace folder
let refreshFilesExplorer = false;
for await (const folder of vscode.workspace.workspaceFolders ?? []) {
if (schemas.includes(folder.uri.scheme)) {
refreshFilesExplorer = true;
}
try {
await checkConnection(true, folder.uri, true);
} catch (_) {
continue;
}
// Loop through all ws folder connections and see if any changed
// Find all "sessions" that are new or orphaned.
// For new ones, establish a connection?
// For orphans, logout and clear the cookies.
let refreshFilesExplorer = false;
const newConnections: Set<string> = new Set();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const newConnections = new Set<string>(); is equivalent and more concise.

const affectedWsFolders: vscode.WorkspaceFolder[] = [];
for (const wsFolder of vscode.workspace.workspaceFolders ?? []) {
const api = new AtelierAPI(wsFolder.uri);
newConnections.add(api.mapKey());
const { serverName } = api.config;
if (
(notIsfs(wsFolder.uri) && affectsConfiguration("objectscript.conn", wsFolder)) ||
(serverName && affectsConfiguration(`intersystems.servers.${serverName}`, wsFolder))
) {
// Connection info changed
affectedWsFolders.push(wsFolder);
if (filesystemSchemas.includes(wsFolder.uri.scheme)) refreshFilesExplorer = true;
}
explorerProvider.refresh();
projectsExplorerProvider.refresh();
if (refreshFilesExplorer) {
// This unavoidably switches to the File Explorer view, so only do it if isfs folders were found
vscode.commands.executeCommand("workbench.files.action.refreshFilesExplorer");
}
// Log out of any sessions that are orphaned
logoutOfSessions(Array.from(cookiesMap.keys()).filter((k) => !newConnections.has(k)));
// Update the connection info for affected workspace folders
// This should create new CSP sessions if needed
for (const wsFolder of affectedWsFolders) {
try {
await checkConnection(true, wsFolder.uri, true);
} catch {
// Errors are handled by checkConnection()
}
updateWebAndAbstractDocsCaches(vscode.workspace.workspaceFolders);
}
explorerProvider.refresh();
projectsExplorerProvider.refresh();
if (refreshFilesExplorer) {
// This unavoidably switches to the File Explorer view, so only do it if isfs folders were found
vscode.commands.executeCommand("workbench.files.action.refreshFilesExplorer");
}
updateWebAndAbstractDocsCaches(affectedWsFolders);
if (affectsConfiguration("objectscript.commentToken")) {
// Update the language configuration for "objectscript" and "objectscript-macros"
macLangConf?.dispose();
Expand Down Expand Up @@ -2037,24 +2035,5 @@ export async function deactivate(): Promise<void> {
intLangConf?.dispose();
disposeDocumentIndex();
// Log out of all CSP sessions
const loggedOut: Set<string> = new Set();
const promises: Promise<any>[] = [];
for (const f of vscode.workspace.workspaceFolders ?? []) {
const api = new AtelierAPI(f.uri);
if (!api.active || !api.cookies.length) continue;
const sessionCookie = api.cookies.find((c) => c.startsWith("CSPSESSIONID-"));
if (!sessionCookie || loggedOut.has(sessionCookie)) continue;
loggedOut.add(sessionCookie);
promises.push(
api.request(
0,
"HEAD",
undefined,
undefined,
// Prefer IRISLogout for servers that support it
semver.lt(api.config.serverVersion, "2018.2.0") ? { CacheLogout: "end" } : { IRISLogout: "end" }
)
);
}
await Promise.allSettled(promises);
await logoutOfSessions();
}