Skip to content
Merged
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
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,10 @@
{
"command": "vscode-objectscript.openISCDocument",
"when": "vscode-objectscript.connectActive && workspaceFolderCount != 0"
},
{
"command": "vscode-objectscript.connectFolderToServerNamespace",
"when": "!vscode-objectscript.connectActive && vscode-objectscript.explorerRootCount == 0 && workspaceFolderCount != 0"
}
],
"view/title": [
Expand Down
25 changes: 24 additions & 1 deletion src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,29 @@ export class AtelierAPI {
this.namespace = namespace;
}

/**
* Manually set the connection spec for this object,
* where `connSpec` is the return value of `getResolvedConnectionSpec()`.
*/
public setConnSpec(serverName: string, connSpec: any): void {
const {
webServer: { scheme, host, port, pathPrefix = "" },
username,
password,
} = connSpec;
this._config.username = username;
this._config.password = password;
this._config.https = scheme == "https";
this._config.host = host;
this._config.port = port;
this._config.pathPrefix = pathPrefix;
this._config.apiVersion = this._config.apiVersion || DEFAULT_API_VERSION;
this._config.serverVersion = this._config.serverVersion || DEFAULT_SERVER_VERSION;
this._config.docker = false;
this._config.active = true;
this._config.serverName = serverName;
}

public get active(): boolean {
const { host = "", port = 0 } = this.config;
return !!this._config.active && host.length > 0 && port > 0;
Expand Down Expand Up @@ -185,7 +208,7 @@ export class AtelierAPI {
this.configName = workspaceFolderName;
const conn = config("conn", workspaceFolderName);
let serverName = workspaceFolderName.toLowerCase();
if (config("intersystems.servers").has(serverName)) {
if (config("intersystems.servers", workspaceFolderName).has(serverName)) {
this.externalServer = true;
} else if (
!(conn["docker-compose"] && extensionContext.extension.extensionKind !== vscode.ExtensionKind.Workspace) &&
Expand Down
19 changes: 1 addition & 18 deletions src/commands/addServerNamespaceToWorkspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
FILESYSTEM_SCHEMA,
FILESYSTEM_READONLY_SCHEMA,
filesystemSchemas,
smExtensionId,
serverManagerApi,
} from "../extension";
import { cspAppsForUri, getWsFolder, handleError, notIsfs } from "../utils";
import { pickProject } from "./project";
Expand All @@ -18,7 +18,6 @@ import { isfsConfig, IsfsUriParam } from "../utils/FileProviderUtil";
* @returns An object containing `serverName` and `namespace`, or `undefined`.
*/
export async function pickServerAndNamespace(message?: string): Promise<{ serverName: string; namespace: string }> {
const serverManagerApi = await getServerManagerApi();
if (!serverManagerApi) {
vscode.window.showErrorMessage(
`${
Expand Down Expand Up @@ -168,22 +167,6 @@ export async function addServerNamespaceToWorkspace(resource?: vscode.Uri): Prom
}
}

export async function getServerManagerApi(): Promise<any> {
const targetExtension = vscode.extensions.getExtension(smExtensionId);
if (!targetExtension) {
return undefined;
}
if (!targetExtension.isActive) {
await targetExtension.activate();
}
const api = targetExtension.exports;

if (!api) {
return undefined;
}
return api;
}

/** Prompt the user to fill in the `path` and `query` of `uri`. */
async function modifyWsFolderUri(uri: vscode.Uri): Promise<vscode.Uri | undefined> {
if (notIsfs(uri)) return;
Expand Down
114 changes: 80 additions & 34 deletions src/commands/connectFolderToServerNamespace.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import * as vscode from "vscode";
import { AtelierAPI } from "../api";
import { panel, resolveConnectionSpec, getResolvedConnectionSpec, smExtensionId } from "../extension";
import { notIsfs } from "../utils";
import {
panel,
resolveConnectionSpec,
getResolvedConnectionSpec,
serverManagerApi,
resolveUsernameAndPassword,
} from "../extension";
import { handleError, isUnauthenticated, notIsfs } from "../utils";

interface ConnSettings {
server: string;
Expand All @@ -14,7 +20,6 @@ export async function connectFolderToServerNamespace(): Promise<void> {
vscode.window.showErrorMessage("No folders in the workspace.", "Dismiss");
return;
}
const serverManagerApi = await getServerManagerApi();
if (!serverManagerApi) {
vscode.window.showErrorMessage(
"Connecting a folder to a server namespace requires the [InterSystems Server Manager extension](https://marketplace.visualstudio.com/items?itemName=intersystems-community.servermanager) to be installed and enabled.",
Expand All @@ -31,42 +36,71 @@ export async function connectFolderToServerNamespace(): Promise<void> {
return {
label: folder.name,
description: folder.uri.fsPath,
detail: !conn.server ? undefined : `Currently connected to ${conn.ns} on ${conn.server}`,
detail:
!conn.server || !conn.active
? "No active server connection"
: `Currently connected to ${conn.ns} on ${conn.server}`,
};
});
if (!items.length) {
vscode.window.showErrorMessage("No local folders in the workspace.", "Dismiss");
return;
}
const pick =
items.length === 1 && !items[0].detail
items.length == 1 && !items[0].detail.startsWith("Currently")
? items[0]
: await vscode.window.showQuickPick(items, { title: "Pick a folder" });
const folder = vscode.workspace.workspaceFolders.find((el) => el.name === pick.label);
// Get user's choice of server
const options: vscode.QuickPickOptions = {};
const serverName: string = await serverManagerApi.pickServer(undefined, options);
const serverName: string = await serverManagerApi.pickServer(folder, options);
if (!serverName) {
return;
}
// Get its namespace list
const uri = vscode.Uri.parse(`isfs://${serverName}/?ns=%SYS`);
await resolveConnectionSpec(serverName);
await resolveConnectionSpec(serverName, undefined, folder);
// Prepare a displayable form of its connection spec as a hint to the user
// This will never return the default value (second parameter) because we only just resolved the connection spec.
const connSpec = getResolvedConnectionSpec(serverName, undefined);
const connDisplayString = `${connSpec.webServer.scheme}://${connSpec.webServer.host}:${connSpec.webServer.port}/${connSpec.webServer.pathPrefix}`;
// Connect and fetch namespaces
const api = new AtelierAPI(uri);
const allNamespaces: string[] | undefined = await api
const api = new AtelierAPI(vscode.Uri.parse(`isfs://${serverName}/?ns=%SYS`));
const serverConf = vscode.workspace
.getConfiguration("intersystems", folder)
.inspect<{ [key: string]: any }>("servers");
if (
serverConf.workspaceFolderValue &&
typeof serverConf.workspaceFolderValue[serverName] == "object" &&
!(serverConf.workspaceValue && typeof serverConf.workspaceValue[serverName] == "object")
) {
// Need to manually set connection info if the server is defined at the workspace folder level
api.setConnSpec(serverName, connSpec);
}
const allNamespaces: string[] = await api
.serverInfo(false)
.then((data) => data.result.content.namespaces)
.catch((reason) => {
// Notify user about serverInfo failure
vscode.window.showErrorMessage(
reason.message || `Failed to fetch namespace list from server at ${connDisplayString}`,
"Dismiss"
);
.catch(async (error) => {
if (error?.statusCode == 401 && isUnauthenticated(api.config.username)) {
// Attempt to resolve username and password and try again
const newSpec = await resolveUsernameAndPassword(api.config.serverName, connSpec);
if (newSpec) {
// We were able to resolve credentials, so try again
api.setConnSpec(api.config.serverName, newSpec);
return api
.serverInfo(false)
.then((data) => data.result.content.namespaces)
.catch(async (err) => {
handleError(err, `Failed to fetch namespace list from server at ${connDisplayString}.`);
return undefined;
});
} else {
handleError(
`Unauthenticated access rejected by '${api.serverId}'.`,
`Failed to fetch namespace list from server at ${connDisplayString}.`
);
return undefined;
}
}
handleError(error, `Failed to fetch namespace list from server at ${connDisplayString}.`);
return undefined;
});
// Clear the panel entry created by the connection
Expand All @@ -90,22 +124,34 @@ export async function connectFolderToServerNamespace(): Promise<void> {
}
// Update folder's config object
const config = vscode.workspace.getConfiguration("objectscript", folder);
const conn: any = config.inspect("conn").workspaceFolderValue;
await config.update("conn", { ...conn, server: serverName, ns: namespace, active: true });
}

async function getServerManagerApi(): Promise<any> {
const targetExtension = vscode.extensions.getExtension(smExtensionId);
if (!targetExtension) {
return undefined;
}
if (!targetExtension.isActive) {
await targetExtension.activate();
}
const api = targetExtension.exports;

if (!api) {
return undefined;
if (vscode.workspace.workspaceFile && items.length == 1) {
// Ask the user if they want to enable the connection at the workspace or folder level.
// Only allow this when there is a single client-side folder in the workspace because
// the server may be configured at the workspace folder level.
const answer = await vscode.window.showQuickPick(
[
{ label: `Workspace Folder ${folder.name}`, detail: folder.uri.toString(true) },
{ label: "Workspace File", detail: vscode.workspace.workspaceFile.toString(true) },
],
{ title: "Store the server connection at the workspace or folder level?" }
);
if (!answer) return;
if (answer.label == "Workspace File") {
// Enable the connection at the workspace level
const conn: any = config.inspect("conn").workspaceValue;
await config.update(
"conn",
{ ...conn, server: serverName, ns: namespace, active: true },
vscode.ConfigurationTarget.Workspace
);
return;
}
}
return api;
// Enable the connection at the workspace folder level
const conn: any = config.inspect("conn").workspaceFolderValue;
await config.update(
"conn",
{ ...conn, server: serverName, ns: namespace, active: true },
vscode.ConfigurationTarget.WorkspaceFolder
);
}
53 changes: 35 additions & 18 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ let reporter: TelemetryReporter;

export let checkingConnection = false;

let serverManagerApi: serverManager.ServerManagerAPI;
export let serverManagerApi: serverManager.ServerManagerAPI;

/** Map of the intersystems.server connection specs we have resolved via the API to that extension */
const resolvedConnSpecs = new Map<string, any>();
Expand All @@ -227,22 +227,26 @@ const resolvedConnSpecs = new Map<string, any>();
* @param serverName authority element of an isfs uri, or `objectscript.conn.server` property, or the name of a root folder with an `objectscript.conn.docker-compose` property object
* @param uri if passed, re-check the `objectscript.conn.docker-compose` case in case servermanager API couldn't do that because we're still running our own `activate` method.
*/
export async function resolveConnectionSpec(serverName: string, uri?: vscode.Uri): Promise<void> {
export async function resolveConnectionSpec(
serverName: string,
uri?: vscode.Uri,
scope?: vscode.ConfigurationScope
): Promise<void> {
if (!serverManagerApi || !serverManagerApi.getServerSpec || serverName === "") {
return;
}
if (resolvedConnSpecs.has(serverName)) {
// Already resolved
return;
}
if (!vscode.workspace.getConfiguration("intersystems.servers", null).has(serverName)) {
if (!vscode.workspace.getConfiguration("intersystems.servers", scope).has(serverName)) {
// When not a defined server see it already resolved as a foldername that matches case-insensitively
if (getResolvedConnectionSpec(serverName, undefined)) {
return;
}
}

let connSpec = await serverManagerApi.getServerSpec(serverName);
let connSpec = await serverManagerApi.getServerSpec(serverName, scope);

if (!connSpec && uri) {
// Caller passed uri as a signal to process any docker-compose settings
Expand Down Expand Up @@ -305,6 +309,24 @@ async function resolvePassword(serverSpec, ignoreUnauthenticated = false): Promi
}
}

/** Resolve credentials for `serverName` and returned the complete connection spec if successful */
export async function resolveUsernameAndPassword(serverName: string, oldSpec: any): Promise<any> {
const newSpec: { name: string; username?: string; password?: string } = {
name: serverName,
username: oldSpec?.username,
};
await resolvePassword(newSpec, true);
if (newSpec.password) {
// Update the connection spec
resolvedConnSpecs.set(serverName, {
...oldSpec,
username: newSpec.username,
password: newSpec.password,
});
return resolvedConnSpecs.get(serverName);
}
}

/** Accessor for the cache of resolved connection specs */
export function getResolvedConnectionSpec(key: string, dflt: any): any {
let spec = resolvedConnSpecs.get(key);
Expand Down Expand Up @@ -462,25 +484,20 @@ export async function checkConnection(
if (isUnauthenticated(username)) {
vscode.window.showErrorMessage(
`Unauthenticated access rejected by '${api.serverId}'.${
!api.externalServer ? " Connection has been disabled." : ""
!api.config.serverName ? " Connection has been disabled." : ""
}`,
"Dismiss"
);
if (api.externalServer) {
if (api.config.serverName) {
// Attempt to resolve a username and password
const newSpec: { name: string; username?: string; password?: string } = {
name: api.config.serverName,
username,
};
await resolvePassword(newSpec, true);
if (newSpec.password) {
// Update the connection spec and try again
const oldSpec = getResolvedConnectionSpec(
api.config.serverName,
vscode.workspace.getConfiguration("intersystems.servers", uri).get(api.config.serverName)
);
const newSpec = await resolveUsernameAndPassword(api.config.serverName, oldSpec);
if (newSpec) {
// We were able to resolve credentials, so try again
await workspaceState.update(wsKey + ":password", newSpec.password);
resolvedConnSpecs.set(api.config.serverName, {
...resolvedConnSpecs.get(api.config.serverName),
username: newSpec.username,
password: newSpec.password,
});
api = new AtelierAPI(apiTarget, false);
await api
.serverInfo(true, serverInfoTimeout)
Expand Down