Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add sas 9 remote (via IOM bridge) support #592

Merged
merged 53 commits into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
dc7b03f
get iom bridge working
Oct 30, 2023
998baf5
add basic profile setup
Oct 30, 2023
3286b16
add password storage
Oct 31, 2023
cfdf4a0
clean up code/fix password prompt
Oct 31, 2023
e4afd38
self code review
Oct 31, 2023
e5a6f0e
fix tests
Oct 31, 2023
303548b
move com->itc
Nov 1, 2023
8347560
add test for fetch file
Nov 1, 2023
217da85
fix tests for windows
Nov 1, 2023
d4eadde
update CHANGELOG / connect-and-run
Nov 1, 2023
0f445a0
Update README
Nov 1, 2023
046459a
Run npm run format
Nov 1, 2023
0b5919f
Rename COMSession -> ITCSession
Nov 1, 2023
c3474bf
Update const/comment/default port
Nov 3, 2023
7cf2255
Update connect-and-run
Nov 3, 2023
16a0309
Use components/ExtensionContext
Nov 3, 2023
591357b
store password when fetched from session
Nov 10, 2023
4fd49ff
introduce namespaced secret storage
Nov 10, 2023
5efd0ec
update how sessions are stored/results are fetched
Nov 10, 2023
5ec2211
wip - implement cancel
Nov 10, 2023
aff5798
clear password on session close
Nov 15, 2023
cf93f62
implement cancel
Nov 10, 2023
4473702
Update documentation/npm run format
Nov 15, 2023
a091f7c
cleanup testing method
Nov 17, 2023
c740bd6
update translations
Nov 20, 2023
0ac2c03
move includes -> endswith
Nov 22, 2023
44cec6a
fix filepath
Nov 22, 2023
010a799
run npm run format
Nov 22, 2023
138ad73
fix: set correct encoding on powershell client
smorrisj Dec 13, 2023
0a00464
chore: fix tests
smorrisj Dec 13, 2023
3830454
chore: run test with en-US locale for deterministic behavior
smorrisj Dec 13, 2023
72887db
fix cancelling
Dec 15, 2023
895d7e8
fix lint/format issues
Dec 15, 2023
85a267b
Merge branch 'main' into feat/sas9itc
scottdover Dec 15, 2023
3dee692
Update cancel process
Dec 22, 2023
bf8ac6a
Update cancel process
Dec 22, 2023
c644317
Merge branch 'main' into feat/sas9itc
scottdover Dec 22, 2023
d1c2520
Merge branch 'main' into feat/sas9itc
scottdover Jan 3, 2024
1ffda86
resolve sas 9 popup error
Jan 8, 2024
4663aec
Merge branch 'feat/sas9itc' of github.com:sassoftware/vscode-sas-exte…
Jan 8, 2024
e5eeff5
fix test
Jan 8, 2024
83eae49
Merge branch 'main' into feat/sas9itc
scottdover Jan 8, 2024
0d7667a
Merge branch 'feat/sas9itc' of https://github.com/sassoftware/vscode-…
Jan 8, 2024
1dfdea8
add chcp 65001 to powershell command
Jan 8, 2024
668b51f
Merge branch 'main' into feat/sas9itc
scottdover Jan 8, 2024
4c7b373
upgrade prettier, run npm run format, fix tests
Jan 8, 2024
3b80433
fix running w/o ods results
Jan 9, 2024
5c554aa
add itc connection test script
Jan 9, 2024
04c5829
fix ods results html
Jan 10, 2024
ad48cd1
collect all bytes before string conversion
Jan 11, 2024
a589058
Revert "collect all bytes before string conversion"
Jan 12, 2024
88c6b54
use filestream over streamwriter
Jan 12, 2024
538597b
update chunk size
Jan 12, 2024
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

- Added the ability to upload and download sas content using the context menu ([#547](https://github.com/sassoftware/vscode-sas-extension/issues/547))
- Added the ability to download results as an html file ([#546](https://github.com/sassoftware/vscode-sas-extension/issues/546))
- Added sas 9.4 remote connection support via ITC and the IOM Bridge protocol ([#592](https://github.com/sassoftware/vscode-sas-extension/pull/592))

## [v1.5.0] - 2023-10-27

Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ Welcome to the SAS Extension for Visual Studio Code! This extension provides sup
- [Code Folding and Code Outline](#code-folding-and-code-outline)
- [Configuring the SAS Extension](#configuring-the-sas-extension)
- [Profiles](/connect-and-run.md/#profiles)
- [Profile Anatomy (Viya)](/connect-and-run.md/#profile-anatomy-sas-viya)
- [Profile Details (Viya)](/connect-and-run.md/#profile-sas-viya)
- [Add New SAS Viya Profile](/connect-and-run.md#add-new-sas-viya-profile)
- [Profile Anatomy (SAS 9.4 Remote)](/connect-and-run.md/#profile-anatomy-sas-94-remote)
- [Add New SAS 9.4 Remote Profile](/connect-and-run.md#add-new-sas-94-remote-profile)
- [Profile Anatomy (SAS 9.4 Local)](/connect-and-run.md/#profile-anatomy-sas-94-local)
- [Profile Details (SAS 9.4 Remote SSH)](/connect-and-run.md/#profile-sas-94-remote---ssh)
- [Add New SAS 9.4 Remote SSH Profile](/connect-and-run.md#add-new-sas-94-remote---ssh-profile)
- [Profile Details (SAS 9.4 Local)](/connect-and-run.md/#profile-sas-94-local)
- [Add New SAS 9.4 Local Profile](/connect-and-run.md/#add-new-sas-94-local-profile)
- [Profile Details (SAS 9.4 Remote IOM)](/connect-and-run.md/#profile-sas-94-remote---iom)
- [Add New SAS 9.4 Remote IOM Profile](/connect-and-run.md/#add-new-sas-94-remote---iom-profile)
- [Delete SAS Profile](/connect-and-run.md#delete-connection-profile)
- [Switch Current SAS Profile](/connect-and-run.md#switch-current-connection-profile)
- [Update SAS Profile](/connect-and-run.md#update-connection-profile)
Expand Down
34 changes: 9 additions & 25 deletions client/src/components/AuthProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
Disposable,
Event,
EventEmitter,
SecretStorage,
commands,
workspace,
} from "vscode";
Expand All @@ -17,6 +16,7 @@ import { profileConfig } from "../commands/profile";
import { ConnectionType } from "../components/profile";
import { getTokens, refreshToken } from "../connection/rest/auth";
import { getCurrentUser } from "../connection/rest/identities";
import { getSecretStorage } from "./ExtensionContext";

const SECRET_KEY = "SASAuth";

Expand All @@ -27,6 +27,7 @@ interface SASAuthSession extends AuthenticationSession {
export class SASAuthProvider implements AuthenticationProvider, Disposable {
static id = "SAS";

private readonly secretStorage;
private _disposables: Disposable[];
private _lastSession: SASAuthSession | undefined;
private _onDidChangeSessions =
Expand All @@ -35,7 +36,7 @@ export class SASAuthProvider implements AuthenticationProvider, Disposable {
return this._onDidChangeSessions.event;
}

constructor(private readonly secretStorage: SecretStorage) {
constructor() {
this._disposables = [
this._onDidChangeSessions,
workspace.onDidChangeConfiguration((event: ConfigurationChangeEvent) => {
Expand All @@ -52,6 +53,7 @@ export class SASAuthProvider implements AuthenticationProvider, Disposable {
}
}),
];
this.secretStorage = getSecretStorage<SASAuthSession>(SECRET_KEY);
}

dispose(): void {
Expand Down Expand Up @@ -80,7 +82,7 @@ export class SASAuthProvider implements AuthenticationProvider, Disposable {
}

private async _getSessions(): Promise<readonly AuthenticationSession[]> {
const sessions = await this.getStoredSessions();
const sessions = await this.secretStorage.getNamespaceData();
if (!sessions) {
return [];
}
Expand Down Expand Up @@ -152,18 +154,11 @@ export class SASAuthProvider implements AuthenticationProvider, Disposable {
}

private async writeSession(session: SASAuthSession): Promise<void> {
const storedSessions = await this.getStoredSessions();

const sessions = {
...(storedSessions || {}),
[session.id]: session,
};

await this.secretStorage.store(SECRET_KEY, JSON.stringify(sessions));
this.secretStorage.store(session.id, session);
}

async removeSession(sessionId: string, silent?: boolean): Promise<void> {
const sessions = await this.getStoredSessions();
const sessions = await this.secretStorage.getNamespaceData();
if (!sessions) {
return;
}
Expand All @@ -178,14 +173,14 @@ export class SASAuthProvider implements AuthenticationProvider, Disposable {
if (!silent) {
// Triggered by user sign out from the Accounts menu
// VS Code will sign out all sessions by this account
Object.values(sessions).forEach((s) => {
Object.values(sessions).forEach((s: SASAuthSession) => {
if (s.account.id === session.account.id) {
delete sessions[s.id];
}
});
}

await this.secretStorage.store(SECRET_KEY, JSON.stringify(sessions));
await this.secretStorage.setNamespaceData(sessions);

this._lastSession = undefined;
this._onDidChangeSessions.fire({
Expand All @@ -198,15 +193,4 @@ export class SASAuthProvider implements AuthenticationProvider, Disposable {
commands.executeCommand("setContext", "SAS.authorized", false);
}
}

private async getStoredSessions(): Promise<
Record<string, SASAuthSession> | undefined
> {
const storedSessionData = await this.secretStorage.get(SECRET_KEY);
if (!storedSessionData) {
return;
}

return JSON.parse(storedSessionData);
}
}
40 changes: 39 additions & 1 deletion client/src/components/ExtensionContext.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Copyright © 2023, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { ExtensionContext } from "vscode";
import { ExtensionContext, Uri } from "vscode";

let context: ExtensionContext;

Expand All @@ -26,3 +26,41 @@ export async function getContextValue(
): Promise<string | undefined> {
return context.workspaceState.get(key);
}

export function getGlobalStorageUri(): Uri {
return context.globalStorageUri;
}

export function getSecretStorage<T = string>(namespace: string) {
const getNamespaceData = async (): Promise<Record<string, T> | undefined> => {
const storedSessionData = await context.secrets.get(namespace);
if (!storedSessionData) {
return;
}

return JSON.parse(storedSessionData);
};
const setNamespaceData = async (data: Record<string, T>) => {
await context.secrets.store(namespace, JSON.stringify(data));
};

const get = async (key: string): Promise<T | undefined> => {
const data = await getNamespaceData();
if (!data) {
return;
}

return data[key];
};

const store = async (key: string, value: T) => {
const data = await getNamespaceData();
const newData = {
...(data || {}),
[key]: value,
};
await context.secrets.store(namespace, JSON.stringify(newData));
};

return { setNamespaceData, getNamespaceData, get, store };
}
56 changes: 48 additions & 8 deletions client/src/components/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,16 @@ export const EXTENSION_PROFILES_CONFIG_KEY = "profiles";
export const EXTENSION_ACTIVE_PROFILE_CONFIG_KEY = "activeProfile";

enum ConnectionOptions {
SAS9COM = "SAS 9.4 (local)",
SAS9IOM = "SAS 9.4 (remote - IOM)",
SAS9SSH = "SAS 9.4 (remote - SSH)",
SASViya = "SAS Viya",
SAS94Remote = "SAS 9.4 (remote)",
SAS9COM = "SAS 9.4 (local - COM)",
}

const CONNECTION_PICK_OPTS: string[] = [
ConnectionOptions.SASViya,
ConnectionOptions.SAS94Remote,
ConnectionOptions.SAS9SSH,
ConnectionOptions.SAS9IOM,
ConnectionOptions.SAS9COM,
];

Expand All @@ -33,6 +35,7 @@ const CONNECTION_PICK_OPTS: string[] = [
*/
export const DEFAULT_COMPUTE_CONTEXT = "SAS Job Execution compute context";
export const DEFAULT_SSH_PORT = "22";
export const DEFAULT_IOM_PORT = "8591";

/**
* Dictionary is a type that maps a generic object with a string key.
Expand All @@ -53,9 +56,10 @@ export enum AuthType {
* Enum that represents the connection type for a profile.
*/
export enum ConnectionType {
COM = "com",
IOM = "iom",
Rest = "rest",
SSH = "ssh",
COM = "com",
}

/**
Expand Down Expand Up @@ -91,7 +95,14 @@ export interface COMProfile extends BaseProfile {
host: string;
}

export type Profile = ViyaProfile | SSHProfile | COMProfile;
export interface IOMProfile extends BaseProfile {
connectionType: ConnectionType.IOM;
host: string;
username: string;
port: number;
}

export type Profile = ViyaProfile | SSHProfile | COMProfile | IOMProfile;

export enum AutoExecType {
File = "file",
Expand Down Expand Up @@ -573,6 +584,32 @@ export class ProfileConfig {
} else if (profileClone.connectionType === ConnectionType.COM) {
profileClone.sasOptions = [];
profileClone.host = "localhost"; //once remote support rolls out this should be set via prompting
await this.upsertProfile(name, profileClone);
} else if (profileClone.connectionType === ConnectionType.IOM) {
profileClone.sasOptions = [];
profileClone.host = await createInputTextBox(
ProfilePromptType.Host,
profileClone.host,
);
if (!profileClone.host) {
return;
}

profileClone.port = parseInt(
await createInputTextBox(ProfilePromptType.Port, DEFAULT_IOM_PORT),
);
if (isNaN(profileClone.port)) {
return;
}

profileClone.username = await createInputTextBox(
ProfilePromptType.Username,
profileClone.username,
);
if (profileClone.username === undefined) {
return;
}

await this.upsertProfile(name, profileClone);
}
}
Expand All @@ -588,6 +625,7 @@ export class ProfileConfig {
switch (activeProfile.connectionType) {
case ConnectionType.SSH:
case ConnectionType.COM:
case ConnectionType.IOM:
return activeProfile.host;
case ConnectionType.Rest:
return activeProfile.endpoint;
Expand Down Expand Up @@ -737,9 +775,9 @@ const input: ProfilePromptInput = {
description: l10n.t("Select a Connection Type."),
},
[ProfilePromptType.Host]: {
title: l10n.t("SAS 9 SSH Server"),
title: l10n.t("SAS 9 Server"),
placeholder: l10n.t("Enter the server name"),
description: l10n.t("Enter the name of the SAS 9 SSH server."),
description: l10n.t("Enter the name of the SAS 9 server."),
},
[ProfilePromptType.SASPath]: {
title: l10n.t("Server Path"),
Expand Down Expand Up @@ -772,10 +810,12 @@ function mapQuickPickToEnum(connectionTypePickInput: string): ConnectionType {
switch (connectionTypePickInput) {
case ConnectionOptions.SASViya:
return ConnectionType.Rest;
case ConnectionOptions.SAS94Remote:
case ConnectionOptions.SAS9SSH:
return ConnectionType.SSH;
case ConnectionOptions.SAS9COM:
return ConnectionType.COM;
case ConnectionOptions.SAS9IOM:
return ConnectionType.IOM;
default:
return undefined;
}
Expand Down
12 changes: 11 additions & 1 deletion client/src/components/utils/sasCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { ColorThemeKind, l10n, window, workspace } from "vscode";

import { isAbsolute } from "path";
import { v4 } from "uuid";

import { getHtmlStyle, isOutputHtmlEnabled } from "./settings";

Expand Down Expand Up @@ -48,14 +49,23 @@ export function wrapCodeWithOutputHtml(code: string): string {
const htmlStyleOption = generateHtmlStyleOption();
return `title;footnote;ods _all_ close;
ods graphics on;
ods html5${htmlStyleOption} options(bitmap_mode='inline' svg_mode='inline');
ods html5${htmlStyleOption} options(bitmap_mode='inline' svg_mode='inline') body="${v4()}.htm";
${code}
;*';*";*/;run;quit;ods html5 close;`;
} else {
return code;
}
}

export function extractOutputHtmlFileName(
line: string,
defaultValue: string,
): string {
return (
line.match(/body="(.{8}-.{4}-.{4}-.{4}-.{12}).htm"/)?.[1] ?? defaultValue
Copy link
Contributor

Choose a reason for hiding this comment

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

This is probably ok for now, but at some point in the future, we should probably use the ODS IOM events instead (namely ODSComplete), similar to the way EG and SAS Studio handle. Example C# code:

ODS_1_1 _odsService = (ODS_1_1)ws.ODS;
CIODSFileEvents_1_1_Event _odsFileEventsFor93 = ws.ODS as CIODSFileEvents_1_1_Event;
_odsService.FileOpen += HandleOdsFileOpen;
_odsService.FileClose += HandleOdsFileClose;
_odsService.DirectoryBegin += HandleOdsDirectoryBegin;
_odsService.AnchorElement += HandleOdsAnchorElement;
_odsFileEventsFor93.ODSComplete += HandleOdsComplete;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hey @clangsmith . Thanks for this. I linked this comment to the IOM library panel issue and will take a look when I'm working on that.

);
}

export async function wrapCodeWithPreambleAndPostamble(
code: string,
preamble?: string,
Expand Down
Loading