From dc7b03f3b13eaf61f32e13bb0f122790b1e3610a Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Mon, 30 Oct 2023 14:36:54 -0400 Subject: [PATCH 01/46] get iom bridge working --- client/src/connection/com/index.ts | 57 +++++++++++++++++++++++++---- client/src/connection/com/script.ts | 31 +++++++++++++--- client/src/node/extension.ts | 3 ++ 3 files changed, 79 insertions(+), 12 deletions(-) diff --git a/client/src/connection/com/index.ts b/client/src/connection/com/index.ts index 4b9c983d2..4643af0fd 100644 --- a/client/src/connection/com/index.ts +++ b/client/src/connection/com/index.ts @@ -9,6 +9,12 @@ import { updateStatusBarItem } from "../../components/StatusBarItem"; import { Session } from "../session"; import { scriptContent } from "./script"; +import * as vscode from 'vscode'; +import { extensionContext } from '../../node/extension'; + + +import { v4 } from "uuid"; + const endCode = "--vscode-sas-extension-submit-end--"; let sessionInstance: COMSession; @@ -71,10 +77,17 @@ export class COMSession extends Session { */ if (!this._workDirectory) { this._shellProcess.stdin.write( - `$profileHost = "${this._config.host}"\n`, + // `$profileHost = "${this._config.host}"\n`, + `$profileHost = ""\n`, + ); + this._shellProcess.stdin.write( + `$port = 8591\n`, + ); + this._shellProcess.stdin.write( + `$protocol = 2\n`, ); this._shellProcess.stdin.write( - "$runner.Setup($profileHost)\n", + "$runner.Setup($profileHost,$port,$protocol)\n", this.onWriteComplete, ); this._shellProcess.stdin.write( @@ -111,7 +124,7 @@ export class COMSession extends Session { return new Promise((resolve, reject) => { this._runResolve = resolve; this._runReject = reject; - + //write ODS output to work so that the session cleans up after itself const codeWithODSPath = code.replace( "ods html5;", @@ -249,11 +262,41 @@ do { /** * Fetches the ODS output results for the latest html results file. */ - private fetchResults = () => { - const htmlResults = readFileSync( - resolve(this._workDirectory, this._html5FileName + ".htm"), - { encoding: "utf-8" }, + private fetchResults = async () => { + await vscode.workspace.fs.createDirectory(extensionContext.globalStorageUri); + const outputFileUri = vscode.Uri.joinPath(extensionContext.globalStorageUri, `${v4()}.htm`); + this._shellProcess.stdin.write( + ` + $filePath = "${resolve(this._workDirectory, this._html5FileName + ".htm")}"\n + $outputFile = "${outputFileUri.fsPath}"\n + $runner.FetchResultsFile($filePath, $outputFile)\n + `, + this.onWriteComplete, ); + + // console.log(vscode.Uri.joinPath(extensionContext.storageUri, 'sashtml.htm').fsPath); + const file = await new Promise(resolve => { + const start = Date.now(); + const maxTime = 10 * 1000; + const interval = setInterval(async () => { + try { + const file = await vscode.workspace.fs.readFile(outputFileUri); + clearInterval(interval); + resolve(file); + } catch (e) { + // Intentionally blank + if (Date.now() - maxTime > start) { + clearInterval(interval); + resolve(null); + } + } + }, 1000); + }); + + // Error checking + + const htmlResults = file.toString(); + vscode.workspace.fs.delete(outputFileUri); const runResult: RunResult = {}; if (htmlResults.search('<*id="IDX*.+">') !== -1) { runResult.html5 = htmlResults; diff --git a/client/src/connection/com/script.ts b/client/src/connection/com/script.ts index e3ab02b39..7fcf50028 100644 --- a/client/src/connection/com/script.ts +++ b/client/src/connection/com/script.ts @@ -17,21 +17,21 @@ class SASRunner{ $varLogs = $this.FlushLog(4096) Write-Host $varLogs } - [void]Setup([string]$profileHost) { + [void]Setup([string]$profileHost, [int]$port, [int]$protocol) { try { # create the Integration Technologies objects $objFactory = New-Object -ComObject SASObjectManager.ObjectFactoryMulti2 $objServerDef = New-Object -ComObject SASObjectManager.ServerDef $objServerDef.MachineDNSName = $profileHost # SAS Workspace node - $objServerDef.Port = 0 # workspace server port - $objServerDef.Protocol = 0 # 0 = COM protocol + $objServerDef.Port = $port # workspace server port + $objServerDef.Protocol = $protocol # 0 = COM protocol # Class Identifier for SAS Workspace $objServerDef.ClassIdentifier = "440196d4-90f0-11d0-9f41-00a024bb830c" # create and connect to the SAS session $this.objSAS = $objFactory.CreateObjectByServer( - "Local", # server name + "itcconnect", # server name $true, $objServerDef, # built server definition "", # user ID @@ -106,6 +106,27 @@ class SASRunner{ Write-Host $log } while ($log.Length -gt 0) } -} + [void]FetchResultsFile([string]$filePath, [string]$outputFile) { + $fileRef = "" + $objFile = $this.objSAS.FileService.AssignFileref("outfile", "DISK", $filePath, "", [ref] $fileRef) + $objStream = $objFile.OpenBinaryStream(1); + [Byte[]] $bytes = 0x0 + + $endOfFile = $false + $byteCount = 0 + $outStream = [System.IO.StreamWriter] $outputFile + do + { + $objStream.Read(1024, [ref] $bytes) + $outStream.Write([System.Text.Encoding]::UTF8.GetString($bytes)) + $endOfFile = $bytes.Length -lt 1024 + $byteCount = $byteCount + $bytes.Length + } while (-not $endOfFile) + + $objStream.Close() + $outStream.Close() + $this.objSAS.FileService.DeassignFileref($objFile.FilerefName) + } +} `; diff --git a/client/src/node/extension.ts b/client/src/node/extension.ts index 87af091f9..baf1f1e60 100644 --- a/client/src/node/extension.ts +++ b/client/src/node/extension.ts @@ -54,8 +54,11 @@ import { SAS_TASK_TYPE } from "../components/tasks/SasTasks"; let client: LanguageClient; +export let extensionContext: ExtensionContext | undefined; + export function activate(context: ExtensionContext): void { // The server is implemented in node + extensionContext = context; const serverModule = context.asAbsolutePath( path.join("server", "dist", "node", "server.js"), ); From 998baf503de7826503c54c897a683c68f0f04204 Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Mon, 30 Oct 2023 14:50:54 -0400 Subject: [PATCH 02/46] add basic profile setup --- client/src/components/profile.ts | 49 +++++++++++++-- client/src/connection/com/index.ts | 96 ++++++++++++++++++++--------- client/src/connection/com/script.ts | 12 ++-- client/src/connection/index.ts | 6 +- package.json | 38 +++++++++++- 5 files changed, 159 insertions(+), 42 deletions(-) diff --git a/client/src/components/profile.ts b/client/src/components/profile.ts index 96ab349a0..35d00d42b 100644 --- a/client/src/components/profile.ts +++ b/client/src/components/profile.ts @@ -18,13 +18,15 @@ export const EXTENSION_ACTIVE_PROFILE_CONFIG_KEY = "activeProfile"; enum ConnectionOptions { SASViya = "SAS Viya", - SAS94Remote = "SAS 9.4 (remote)", - SAS9COM = "SAS 9.4 (local - COM)", + SAS94Remote = "SAS 9.4 (remote - SSH)", + SAS9IOM = "SAS 9.4 (remote - IOM)", + SAS9COM = "SAS 9.4 (local)", } const CONNECTION_PICK_OPTS: string[] = [ ConnectionOptions.SASViya, ConnectionOptions.SAS94Remote, + ConnectionOptions.SAS9IOM, ConnectionOptions.SAS9COM, ]; @@ -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. @@ -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", } /** @@ -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", @@ -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); } } @@ -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; @@ -775,7 +813,8 @@ function mapQuickPickToEnum(connectionTypePickInput: string): ConnectionType { case ConnectionOptions.SAS94Remote: return ConnectionType.SSH; case ConnectionOptions.SAS9COM: - return ConnectionType.COM; + case ConnectionOptions.SAS9IOM: + return ConnectionType.IOM; default: return undefined; } diff --git a/client/src/connection/com/index.ts b/client/src/connection/com/index.ts index 4643af0fd..b5a1133f7 100644 --- a/client/src/connection/com/index.ts +++ b/client/src/connection/com/index.ts @@ -1,28 +1,33 @@ // Copyright © 2023, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import * as vscode from "vscode"; + import { ChildProcessWithoutNullStreams, spawn } from "child_process"; -import { readFileSync } from "fs"; import { resolve } from "path"; +import { v4 } from "uuid"; import { BaseConfig, RunResult } from ".."; import { updateStatusBarItem } from "../../components/StatusBarItem"; +import { extensionContext } from "../../node/extension"; import { Session } from "../session"; import { scriptContent } from "./script"; -import * as vscode from 'vscode'; -import { extensionContext } from '../../node/extension'; - - -import { v4 } from "uuid"; - const endCode = "--vscode-sas-extension-submit-end--"; let sessionInstance: COMSession; +export enum ITCProtocol { + COM = 0, + IOMBridge = 2, +} + /** * Configuration parameters for this connection provider */ export interface Config extends BaseConfig { host: string; + port: number; + username: string; + protocol: ITCProtocol; } export class COMSession extends Session { @@ -46,7 +51,7 @@ export class COMSession extends Session { * @returns void promise. */ public setup = async (): Promise => { - return new Promise((resolve, reject) => { + return new Promise(async (resolve, reject) => { this._runResolve = resolve; this._runReject = reject; @@ -70,24 +75,29 @@ export class COMSession extends Session { this.onWriteComplete, ); + const password = await this.fetchPassword(); + /* - There are cases where the higher level run command will invoke setup multiple times. - Avoid re-initializing the session when this happens. In a first run scenario a work dir - will not exist. The work dir should only be deleted when close is invoked. - */ + * There are cases where the higher level run command will invoke setup multiple times. + * Avoid re-initializing the session when this happens. In a first run scenario a work dir + * will not exist. The work dir should only be deleted when close is invoked. + */ if (!this._workDirectory) { + const { host, port, protocol, username } = this._config; + const serverName = + protocol === ITCProtocol.COM ? "ITC Local" : "ITC IOM Bridge"; + this._shellProcess.stdin.write(`$profileHost = "${host}"\n`); + this._shellProcess.stdin.write(`$port = ${port}\n`); + this._shellProcess.stdin.write(`$protocol = ${protocol}\n`); + this._shellProcess.stdin.write(`$username = "${username}"\n`); + this._shellProcess.stdin.write(`$password = "${password}"\n`); this._shellProcess.stdin.write( - // `$profileHost = "${this._config.host}"\n`, - `$profileHost = ""\n`, - ); - this._shellProcess.stdin.write( - `$port = 8591\n`, - ); - this._shellProcess.stdin.write( - `$protocol = 2\n`, + `$serverName = "${ + protocol === ITCProtocol.COM ? "ITC Local" : "ITC IOM Bridge" + }"\n`, ); this._shellProcess.stdin.write( - "$runner.Setup($profileHost,$port,$protocol)\n", + `$runner.Setup($profileHost,$username,$password,$port,$protocol,$serverName)\n`, this.onWriteComplete, ); this._shellProcess.stdin.write( @@ -114,6 +124,21 @@ export class COMSession extends Session { }); }; + private fetchPassword = async (): Promise => { + if (this._config.protocol === ITCProtocol.COM) { + return ""; + } + + const password = await vscode.window.showInputBox({ + title: "Enter a password", + placeHolder: "Thing", + prompt: "Do something", + ignoreFocusOut: true, + }); + + return password; + }; + /** * Executes the given input code. * @param code A string of SAS code to execute. @@ -124,7 +149,7 @@ export class COMSession extends Session { return new Promise((resolve, reject) => { this._runResolve = resolve; this._runReject = reject; - + //write ODS output to work so that the session cleans up after itself const codeWithODSPath = code.replace( "ods html5;", @@ -263,8 +288,13 @@ do { * Fetches the ODS output results for the latest html results file. */ private fetchResults = async () => { - await vscode.workspace.fs.createDirectory(extensionContext.globalStorageUri); - const outputFileUri = vscode.Uri.joinPath(extensionContext.globalStorageUri, `${v4()}.htm`); + await vscode.workspace.fs.createDirectory( + extensionContext.globalStorageUri, + ); + const outputFileUri = vscode.Uri.joinPath( + extensionContext.globalStorageUri, + `${v4()}.htm`, + ); this._shellProcess.stdin.write( ` $filePath = "${resolve(this._workDirectory, this._html5FileName + ".htm")}"\n @@ -275,7 +305,7 @@ do { ); // console.log(vscode.Uri.joinPath(extensionContext.storageUri, 'sashtml.htm').fsPath); - const file = await new Promise(resolve => { + const file = await new Promise((resolve) => { const start = Date.now(); const maxTime = 10 * 1000; const interval = setInterval(async () => { @@ -294,7 +324,7 @@ do { }); // Error checking - + const htmlResults = file.toString(); vscode.workspace.fs.delete(outputFileUri); const runResult: RunResult = {}; @@ -311,10 +341,20 @@ do { * @param c Instance denoting configuration parameters for this connection profile. * @returns created COM session. */ -export const getSession = (c: Config): Session => { +export const getSession = ( + c: Partial, + protocol: ITCProtocol, +): Session => { + const defaults = { + host: "localhost", + port: 0, + username: "", + protocol, + }; + if (!sessionInstance) { sessionInstance = new COMSession(); } - sessionInstance.config = c; + sessionInstance.config = { ...defaults, ...c }; return sessionInstance; }; diff --git a/client/src/connection/com/script.ts b/client/src/connection/com/script.ts index 7fcf50028..b17593689 100644 --- a/client/src/connection/com/script.ts +++ b/client/src/connection/com/script.ts @@ -17,25 +17,25 @@ class SASRunner{ $varLogs = $this.FlushLog(4096) Write-Host $varLogs } - [void]Setup([string]$profileHost, [int]$port, [int]$protocol) { + [void]Setup([string]$profileHost, [string]$username, [string]$password, [int]$port, [int]$protocol, [string]$serverName) { try { # create the Integration Technologies objects $objFactory = New-Object -ComObject SASObjectManager.ObjectFactoryMulti2 $objServerDef = New-Object -ComObject SASObjectManager.ServerDef $objServerDef.MachineDNSName = $profileHost # SAS Workspace node - $objServerDef.Port = $port # workspace server port - $objServerDef.Protocol = $protocol # 0 = COM protocol + $objServerDef.Port = $port # workspace server port + $objServerDef.Protocol = $protocol # 0 = COM protocol # Class Identifier for SAS Workspace $objServerDef.ClassIdentifier = "440196d4-90f0-11d0-9f41-00a024bb830c" # create and connect to the SAS session $this.objSAS = $objFactory.CreateObjectByServer( - "itcconnect", # server name + $serverName, # server name $true, $objServerDef, # built server definition - "", # user ID - "" # password + $username, + $password ) } catch { diff --git a/client/src/connection/index.ts b/client/src/connection/index.ts index d9a9a12fd..2f30e58b9 100644 --- a/client/src/connection/index.ts +++ b/client/src/connection/index.ts @@ -9,7 +9,7 @@ import { ViyaProfile, toAutoExecLines, } from "../components/profile"; -import { getSession as getCOMSession } from "./com"; +import { ITCProtocol, getSession as getITCSession } from "./com"; import { Config as RestConfig, getSession as getRestSession } from "./rest"; import { Error2 as ComputeError, @@ -54,7 +54,9 @@ export function getSession(): Session { case ConnectionType.SSH: return getSSHSession(validProfile.profile); case ConnectionType.COM: - return getCOMSession(validProfile.profile); + return getITCSession(validProfile.profile, ITCProtocol.COM); + case ConnectionType.IOM: + return getITCSession(validProfile.profile, ITCProtocol.IOMBridge); default: throw new Error( l10n.t("Invalid connectionType. Check Profile settings."), diff --git a/package.json b/package.json index e9c9ec91b..75f9a7964 100644 --- a/package.json +++ b/package.json @@ -170,7 +170,8 @@ "enum": [ "rest", "ssh", - "com" + "com", + "iom" ] }, "sasOptions": { @@ -323,6 +324,41 @@ } } }, + { + "if": { + "properties": { + "connectionType": { + "const": "iom" + } + } + }, + "then": { + "required": [ + "host", + "username", + "port" + ], + "properties": { + "host": { + "type": "string", + "default": "", + "description": "%configuration.SAS.connectionProfiles.profiles.ssh.host%" + }, + "username": { + "type": "string", + "default": "", + "description": "%configuration.SAS.connectionProfiles.profiles.ssh.username%" + }, + "port": { + "type": "number", + "default": 22, + "description": "%configuration.SAS.connectionProfiles.profiles.ssh.port%", + "exclusiveMinimum": 1, + "exclusiveMaximum": 65535 + } + } + } + }, { "if": { "properties": { From 3286b16fde396a802b8fa23e7e14a70b61f62003 Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Tue, 31 Oct 2023 09:47:09 -0400 Subject: [PATCH 03/46] add password storage --- client/src/connection/com/index.ts | 33 ++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/client/src/connection/com/index.ts b/client/src/connection/com/index.ts index b5a1133f7..abfc27652 100644 --- a/client/src/connection/com/index.ts +++ b/client/src/connection/com/index.ts @@ -12,6 +12,8 @@ import { extensionContext } from "../../node/extension"; import { Session } from "../session"; import { scriptContent } from "./script"; +const PASSWORD_KEY = "ITC_PASSWORD_KEY"; + const endCode = "--vscode-sas-extension-submit-end--"; let sessionInstance: COMSession; @@ -37,9 +39,11 @@ export class COMSession extends Session { private _runResolve: ((value?) => void) | undefined; private _runReject: ((reason?) => void) | undefined; private _workDirectory: string; + private password: string; constructor() { super(); + this.password = ""; } public set config(value: Config) { @@ -51,13 +55,14 @@ export class COMSession extends Session { * @returns void promise. */ public setup = async (): Promise => { - return new Promise(async (resolve, reject) => { + const password = await this.fetchPassword(); + return new Promise((resolve, reject) => { this._runResolve = resolve; this._runReject = reject; if (this._shellProcess && !this._shellProcess.killed) { resolve(); - return; //manually terminate to avoid executing the code below + return; // manually terminate to avoid executing the code below } this._shellProcess = spawn("powershell.exe /nologo -Command -", { @@ -75,8 +80,6 @@ export class COMSession extends Session { this.onWriteComplete, ); - const password = await this.fetchPassword(); - /* * There are cases where the higher level run command will invoke setup multiple times. * Avoid re-initializing the session when this happens. In a first run scenario a work dir @@ -84,8 +87,6 @@ export class COMSession extends Session { */ if (!this._workDirectory) { const { host, port, protocol, username } = this._config; - const serverName = - protocol === ITCProtocol.COM ? "ITC Local" : "ITC IOM Bridge"; this._shellProcess.stdin.write(`$profileHost = "${host}"\n`); this._shellProcess.stdin.write(`$port = ${port}\n`); this._shellProcess.stdin.write(`$protocol = ${protocol}\n`); @@ -124,19 +125,33 @@ export class COMSession extends Session { }); }; + private storePassword = async () => { + await extensionContext.secrets.store(PASSWORD_KEY, this.password); + }; + + private clearPassword = async () => { + await extensionContext.secrets.delete(PASSWORD_KEY); + this.password = ""; + }; + private fetchPassword = async (): Promise => { if (this._config.protocol === ITCProtocol.COM) { return ""; } - const password = await vscode.window.showInputBox({ + const storedPassword = await extensionContext.secrets.get(PASSWORD_KEY); + if (storedPassword) { + return storedPassword; + } + + this.password = await vscode.window.showInputBox({ title: "Enter a password", placeHolder: "Thing", prompt: "Do something", ignoreFocusOut: true, }); - return password; + return this.password; }; /** @@ -229,6 +244,7 @@ do { private onShellStdErr = (chunk: Buffer): void => { const msg = chunk.toString(); console.warn("shellProcess stderr: " + msg); + this.clearPassword(); this._runReject( new Error( "There was an error executing the SAS Program.\nSee console log for more details.", @@ -332,6 +348,7 @@ do { runResult.html5 = htmlResults; runResult.title = "Result"; } + this.storePassword(); this._runResolve(runResult); }; } From cfdf4a0d277e7c4a1db6efc6e7582df49fcd8eac Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Tue, 31 Oct 2023 12:08:23 -0400 Subject: [PATCH 04/46] clean up code/fix password prompt --- client/src/connection/com/index.ts | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/client/src/connection/com/index.ts b/client/src/connection/com/index.ts index abfc27652..02f2fd4af 100644 --- a/client/src/connection/com/index.ts +++ b/client/src/connection/com/index.ts @@ -1,6 +1,6 @@ // Copyright © 2023, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import * as vscode from "vscode"; +import { Uri, l10n, window, workspace } from "vscode"; import { ChildProcessWithoutNullStreams, spawn } from "child_process"; import { resolve } from "path"; @@ -144,11 +144,11 @@ export class COMSession extends Session { return storedPassword; } - this.password = await vscode.window.showInputBox({ - title: "Enter a password", - placeHolder: "Thing", - prompt: "Do something", + this.password = await window.showInputBox({ ignoreFocusOut: true, + password: true, + prompt: l10n.t("Enter your password for this connection."), + title: l10n.t("Enter your password"), }); return this.password; @@ -304,10 +304,8 @@ do { * Fetches the ODS output results for the latest html results file. */ private fetchResults = async () => { - await vscode.workspace.fs.createDirectory( - extensionContext.globalStorageUri, - ); - const outputFileUri = vscode.Uri.joinPath( + await workspace.fs.createDirectory(extensionContext.globalStorageUri); + const outputFileUri = Uri.joinPath( extensionContext.globalStorageUri, `${v4()}.htm`, ); @@ -320,13 +318,12 @@ do { this.onWriteComplete, ); - // console.log(vscode.Uri.joinPath(extensionContext.storageUri, 'sashtml.htm').fsPath); const file = await new Promise((resolve) => { const start = Date.now(); const maxTime = 10 * 1000; const interval = setInterval(async () => { try { - const file = await vscode.workspace.fs.readFile(outputFileUri); + const file = await workspace.fs.readFile(outputFileUri); clearInterval(interval); resolve(file); } catch (e) { @@ -342,7 +339,7 @@ do { // Error checking const htmlResults = file.toString(); - vscode.workspace.fs.delete(outputFileUri); + workspace.fs.delete(outputFileUri); const runResult: RunResult = {}; if (htmlResults.search('<*id="IDX*.+">') !== -1) { runResult.html5 = htmlResults; From e4afd385ca451203a9e9254a6e48157116be14fd Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Tue, 31 Oct 2023 12:16:22 -0400 Subject: [PATCH 05/46] self code review --- client/src/components/profile.ts | 1 + client/src/connection/com/index.ts | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/client/src/components/profile.ts b/client/src/components/profile.ts index 35d00d42b..c7f385183 100644 --- a/client/src/components/profile.ts +++ b/client/src/components/profile.ts @@ -813,6 +813,7 @@ function mapQuickPickToEnum(connectionTypePickInput: string): ConnectionType { case ConnectionOptions.SAS94Remote: return ConnectionType.SSH; case ConnectionOptions.SAS9COM: + return ConnectionType.COM; case ConnectionOptions.SAS9IOM: return ConnectionType.IOM; default: diff --git a/client/src/connection/com/index.ts b/client/src/connection/com/index.ts index 02f2fd4af..71a280b50 100644 --- a/client/src/connection/com/index.ts +++ b/client/src/connection/com/index.ts @@ -144,12 +144,13 @@ export class COMSession extends Session { return storedPassword; } - this.password = await window.showInputBox({ - ignoreFocusOut: true, - password: true, - prompt: l10n.t("Enter your password for this connection."), - title: l10n.t("Enter your password"), - }); + this.password = + (await window.showInputBox({ + ignoreFocusOut: true, + password: true, + prompt: l10n.t("Enter your password for this connection."), + title: l10n.t("Enter your password"), + })) || ""; return this.password; }; @@ -336,10 +337,11 @@ do { }, 1000); }); - // Error checking + const htmlResults = (file || "").toString(); + if (file) { + workspace.fs.delete(outputFileUri); + } - const htmlResults = file.toString(); - workspace.fs.delete(outputFileUri); const runResult: RunResult = {}; if (htmlResults.search('<*id="IDX*.+">') !== -1) { runResult.html5 = htmlResults; From e5a6f0e00b82b5d66da2e9b94edfa4406713d300 Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Tue, 31 Oct 2023 13:36:34 -0400 Subject: [PATCH 06/46] fix tests --- client/src/connection/com/index.ts | 179 ++++++++++++----------- client/src/connection/index.ts | 13 +- client/test/connection/com/index.test.ts | 56 ++++--- 3 files changed, 143 insertions(+), 105 deletions(-) diff --git a/client/src/connection/com/index.ts b/client/src/connection/com/index.ts index 71a280b50..41e14ed87 100644 --- a/client/src/connection/com/index.ts +++ b/client/src/connection/com/index.ts @@ -1,14 +1,12 @@ // Copyright © 2023, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Uri, l10n, window, workspace } from "vscode"; +import { ExtensionContext, Uri, l10n, window, workspace } from "vscode"; import { ChildProcessWithoutNullStreams, spawn } from "child_process"; import { resolve } from "path"; -import { v4 } from "uuid"; import { BaseConfig, RunResult } from ".."; import { updateStatusBarItem } from "../../components/StatusBarItem"; -import { extensionContext } from "../../node/extension"; import { Session } from "../session"; import { scriptContent } from "./script"; @@ -39,11 +37,13 @@ export class COMSession extends Session { private _runResolve: ((value?) => void) | undefined; private _runReject: ((reason?) => void) | undefined; private _workDirectory: string; - private password: string; + private _password: string; + private _context: ExtensionContext; - constructor() { + constructor(extensionContext: ExtensionContext) { super(); - this.password = ""; + this._password = ""; + this._context = extensionContext; } public set config(value: Config) { @@ -55,83 +55,82 @@ export class COMSession extends Session { * @returns void promise. */ public setup = async (): Promise => { - const password = await this.fetchPassword(); - return new Promise((resolve, reject) => { + const setupPromise = new Promise((resolve, reject) => { this._runResolve = resolve; this._runReject = reject; + }); - if (this._shellProcess && !this._shellProcess.killed) { - resolve(); - return; // manually terminate to avoid executing the code below - } + if (this._shellProcess && !this._shellProcess.killed) { + this._runResolve(); + return; // manually terminate to avoid executing the code below + } + + this._shellProcess = spawn("powershell.exe /nologo -Command -", { + shell: true, + env: process.env, + }); + this._shellProcess.stdout.on("data", this.onShellStdOut); + this._shellProcess.stderr.on("data", this.onShellStdErr); + this._shellProcess.stdin.write(scriptContent + "\n", this.onWriteComplete); + this._shellProcess.stdin.write( + "$runner = New-Object -TypeName SASRunner\n", + this.onWriteComplete, + ); - this._shellProcess = spawn("powershell.exe /nologo -Command -", { - shell: true, - env: process.env, - }); - this._shellProcess.stdout.on("data", this.onShellStdOut); - this._shellProcess.stderr.on("data", this.onShellStdErr); + /* + * There are cases where the higher level run command will invoke setup multiple times. + * Avoid re-initializing the session when this happens. In a first run scenario a work dir + * will not exist. The work dir should only be deleted when close is invoked. + */ + if (!this._workDirectory) { + const { host, port, protocol, username } = this._config; + this._shellProcess.stdin.write(`$profileHost = "${host}"\n`); + this._shellProcess.stdin.write(`$port = ${port}\n`); + this._shellProcess.stdin.write(`$protocol = ${protocol}\n`); + this._shellProcess.stdin.write(`$username = "${username}"\n`); + const password = await this.fetchPassword(); + this._shellProcess.stdin.write(`$password = "${password}"\n`); + this._shellProcess.stdin.write( + `$serverName = "${ + protocol === ITCProtocol.COM ? "ITC Local" : "ITC IOM Bridge" + }"\n`, + ); this._shellProcess.stdin.write( - scriptContent + "\n", + `$runner.Setup($profileHost,$username,$password,$port,$protocol,$serverName)\n`, this.onWriteComplete, ); this._shellProcess.stdin.write( - "$runner = New-Object -TypeName SASRunner\n", + "$runner.ResolveSystemVars()\n", this.onWriteComplete, ); - /* - * There are cases where the higher level run command will invoke setup multiple times. - * Avoid re-initializing the session when this happens. In a first run scenario a work dir - * will not exist. The work dir should only be deleted when close is invoked. - */ - if (!this._workDirectory) { - const { host, port, protocol, username } = this._config; - this._shellProcess.stdin.write(`$profileHost = "${host}"\n`); - this._shellProcess.stdin.write(`$port = ${port}\n`); - this._shellProcess.stdin.write(`$protocol = ${protocol}\n`); - this._shellProcess.stdin.write(`$username = "${username}"\n`); - this._shellProcess.stdin.write(`$password = "${password}"\n`); - this._shellProcess.stdin.write( - `$serverName = "${ - protocol === ITCProtocol.COM ? "ITC Local" : "ITC IOM Bridge" - }"\n`, - ); - this._shellProcess.stdin.write( - `$runner.Setup($profileHost,$username,$password,$port,$protocol,$serverName)\n`, - this.onWriteComplete, - ); + if (this._config.sasOptions?.length > 0) { + const sasOptsInput = `$sasOpts=${this.formatSASOptions( + this._config.sasOptions, + )}\n`; + this._shellProcess.stdin.write(sasOptsInput, this.onWriteComplete); this._shellProcess.stdin.write( - "$runner.ResolveSystemVars()\n", + `$runner.SetOptions($sasOpts)\n`, this.onWriteComplete, ); - - if (this._config.sasOptions?.length > 0) { - const sasOptsInput = `$sasOpts=${this.formatSASOptions( - this._config.sasOptions, - )}\n`; - this._shellProcess.stdin.write(sasOptsInput, this.onWriteComplete); - this._shellProcess.stdin.write( - `$runner.SetOptions($sasOpts)\n`, - this.onWriteComplete, - ); - } } + } - // free objects in the scripting env - process.on("exit", async () => { - close(); - }); + // free objects in the scripting env + process.on("exit", async () => { + close(); }); + + return setupPromise; }; private storePassword = async () => { - await extensionContext.secrets.store(PASSWORD_KEY, this.password); + await this._context.secrets.store(PASSWORD_KEY, this._password); }; private clearPassword = async () => { - await extensionContext.secrets.delete(PASSWORD_KEY); - this.password = ""; + await this._context.secrets.delete(PASSWORD_KEY); + this._password = ""; }; private fetchPassword = async (): Promise => { @@ -139,12 +138,12 @@ export class COMSession extends Session { return ""; } - const storedPassword = await extensionContext.secrets.get(PASSWORD_KEY); + const storedPassword = await this._context.secrets.get(PASSWORD_KEY); if (storedPassword) { return storedPassword; } - this.password = + this._password = (await window.showInputBox({ ignoreFocusOut: true, password: true, @@ -152,7 +151,7 @@ export class COMSession extends Session { title: l10n.t("Enter your password"), })) || ""; - return this.password; + return this._password; }; /** @@ -162,29 +161,31 @@ export class COMSession extends Session { * @returns A promise that eventually resolves to contain the given {@link RunResult} for the input code execution. */ public run = async (code: string): Promise => { - return new Promise((resolve, reject) => { + const runPromise = new Promise((resolve, reject) => { this._runResolve = resolve; this._runReject = reject; + }); - //write ODS output to work so that the session cleans up after itself - const codeWithODSPath = code.replace( - "ods html5;", - `ods html5 path="${this._workDirectory}";`, - ); + //write ODS output to work so that the session cleans up after itself + const codeWithODSPath = code.replace( + "ods html5;", + `ods html5 path="${this._workDirectory}";`, + ); - //write an end mnemonic so that the handler knows when execution has finished - const codeWithEnd = `${codeWithODSPath}\n%put ${endCode};`; - const codeToRun = `$code=\n@'\n${codeWithEnd}\n'@\n`; + //write an end mnemonic so that the handler knows when execution has finished + const codeWithEnd = `${codeWithODSPath}\n%put ${endCode};`; + const codeToRun = `$code=\n@'\n${codeWithEnd}\n'@\n`; - this._shellProcess.stdin.write(codeToRun); - this._shellProcess.stdin.write(`$runner.Run($code)\n`, async (error) => { - if (error) { - this._runReject(error); - } + this._shellProcess.stdin.write(codeToRun); + this._shellProcess.stdin.write(`$runner.Run($code)\n`, async (error) => { + if (error) { + this._runReject(error); + } - await this.fetchLog(); - }); + await this.fetchLog(); }); + + return runPromise; }; /** @@ -251,6 +252,11 @@ do { "There was an error executing the SAS Program.\nSee console log for more details.", ), ); + // If we encountered an error in setup, we need to go through everything again + if (/Setup error/.test(msg)) { + this._shellProcess.kill(); + this._workDirectory = undefined; + } }; /** @@ -305,10 +311,15 @@ do { * Fetches the ODS output results for the latest html results file. */ private fetchResults = async () => { - await workspace.fs.createDirectory(extensionContext.globalStorageUri); + try { + await workspace.fs.readDirectory(this._context.globalStorageUri); + } catch (e) { + await workspace.fs.createDirectory(this._context.globalStorageUri); + } + const outputFileUri = Uri.joinPath( - extensionContext.globalStorageUri, - `${v4()}.htm`, + this._context.globalStorageUri, + `${this._html5FileName}.htm`, ); this._shellProcess.stdin.write( ` @@ -328,7 +339,6 @@ do { clearInterval(interval); resolve(file); } catch (e) { - // Intentionally blank if (Date.now() - maxTime > start) { clearInterval(interval); resolve(null); @@ -360,6 +370,7 @@ do { export const getSession = ( c: Partial, protocol: ITCProtocol, + extensionContext: ExtensionContext, ): Session => { const defaults = { host: "localhost", @@ -369,7 +380,7 @@ export const getSession = ( }; if (!sessionInstance) { - sessionInstance = new COMSession(); + sessionInstance = new COMSession(extensionContext); } sessionInstance.config = { ...defaults, ...c }; return sessionInstance; diff --git a/client/src/connection/index.ts b/client/src/connection/index.ts index 2f30e58b9..6e522222f 100644 --- a/client/src/connection/index.ts +++ b/client/src/connection/index.ts @@ -9,6 +9,7 @@ import { ViyaProfile, toAutoExecLines, } from "../components/profile"; +import { extensionContext } from "../node/extension"; import { ITCProtocol, getSession as getITCSession } from "./com"; import { Config as RestConfig, getSession as getRestSession } from "./rest"; import { @@ -54,9 +55,17 @@ export function getSession(): Session { case ConnectionType.SSH: return getSSHSession(validProfile.profile); case ConnectionType.COM: - return getITCSession(validProfile.profile, ITCProtocol.COM); + return getITCSession( + validProfile.profile, + ITCProtocol.COM, + extensionContext, + ); case ConnectionType.IOM: - return getITCSession(validProfile.profile, ITCProtocol.IOMBridge); + return getITCSession( + validProfile.profile, + ITCProtocol.IOMBridge, + extensionContext, + ); default: throw new Error( l10n.t("Invalid connectionType. Check Profile settings."), diff --git a/client/test/connection/com/index.test.ts b/client/test/connection/com/index.test.ts index ac314e80e..c540477ff 100644 --- a/client/test/connection/com/index.test.ts +++ b/client/test/connection/com/index.test.ts @@ -1,11 +1,16 @@ +import * as vscode from "vscode"; + import { expect } from "chai"; import proc from "child_process"; -import fs from "fs"; +import { unlinkSync, writeFileSync } from "fs"; +import { join } from "path"; import { SinonSandbox, SinonStub, createSandbox } from "sinon"; +import { stubInterface } from "ts-sinon"; -import { getSession } from "../../../src/connection/com"; +import { ITCProtocol, getSession } from "../../../src/connection/com"; import { scriptContent } from "../../../src/connection/com/script"; import { Session } from "../../../src/connection/session"; +import { extensionContext } from "../../../src/node/extension"; describe("COM connection", () => { let sandbox: SinonSandbox; @@ -20,9 +25,7 @@ describe("COM connection", () => { let onDataCallback; beforeEach(() => { - sandbox = createSandbox({ - useFakeTimers: { shouldClearNativeTimers: true }, - }); + sandbox = createSandbox({}); spawnStub = sandbox.stub(proc, "spawn"); @@ -52,7 +55,14 @@ describe("COM connection", () => { host: "localhost", }; - session = getSession(config); + const secretStore = stubInterface(); + const stubbedExtensionContext: vscode.ExtensionContext = { + ...extensionContext, + globalStorageUri: vscode.Uri.from({ scheme: "file", path: __dirname }), + secrets: secretStore, + }; + + session = getSession(config, ITCProtocol.COM, stubbedExtensionContext); session.onLogFn = () => { return; }; @@ -85,35 +95,43 @@ describe("COM connection", () => { expect(stdinStub.args[2][0]).to.deep.equal( `$profileHost = "localhost"\n`, ); - expect(stdinStub.args[3][0]).to.deep.equal( - "$runner.Setup($profileHost)\n", + expect(stdinStub.args[3][0]).to.deep.equal(`$port = 0\n`); + expect(stdinStub.args[4][0]).to.deep.equal(`$protocol = 0\n`); + expect(stdinStub.args[5][0]).to.deep.equal(`$username = ""\n`); + expect(stdinStub.args[6][0]).to.deep.equal(`$password = ""\n`); + expect(stdinStub.args[7][0]).to.deep.equal(`$serverName = "ITC Local"\n`); + expect(stdinStub.args[8][0]).to.deep.equal( + "$runner.Setup($profileHost,$username,$password,$port,$protocol,$serverName)\n", ); - expect(stdinStub.args[4][0]).to.deep.equal( + expect(stdinStub.args[9][0]).to.deep.equal( "$runner.ResolveSystemVars()\n", ); - expect(stdinStub.args[5][0]).to.deep.equal( + expect(stdinStub.args[10][0]).to.deep.equal( `$sasOpts=@("-PAGESIZE=MAX")\n`, ); - expect(stdinStub.args[6][0]).to.deep.equal( + expect(stdinStub.args[11][0]).to.deep.equal( `$runner.SetOptions($sasOpts)\n`, ); }); }); describe("run", () => { - let fsStub: SinonStub; + const html5 = '
'; beforeEach(async () => { - fsStub = sandbox.stub(fs, "readFileSync"); - + writeFileSync(join(__dirname, "sashtml.htm"), html5); const setupPromise = session.setup(); onDataCallback(Buffer.from(`WORKDIR=/work/dir`)); await setupPromise; }); + afterEach(() => { + try { + unlinkSync(join(__dirname, "sashtml.htm")); + } catch (e) { + // Intentionally blank + } + }); it("calls run function from script", async () => { - const html5 = '
'; - fsStub.returns(html5); - const runPromise = session.run( "ods html5;\nproc print data=sashelp.cars;\nrun;", ); @@ -127,11 +145,11 @@ describe("COM connection", () => { expect(runResult.html5).to.equal(html5); expect(runResult.title).to.equal("Result"); - expect(stdinStub.args[7][0]).to.deep.equal( + expect(stdinStub.args[12][0]).to.deep.equal( `$code=\n@'\nods html5 path="/work/dir";\nproc print data=sashelp.cars;\nrun;\n%put --vscode-sas-extension-submit-end--;\n'@\n`, ); - expect(stdinStub.args[8][0]).to.deep.equal(`$runner.Run($code)\n`); + expect(stdinStub.args[13][0]).to.deep.equal(`$runner.Run($code)\n`); }); }); From 303548b86eaba49f91aaf6891ad0507d8cc0d1ff Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Wed, 1 Nov 2023 10:14:51 -0400 Subject: [PATCH 07/46] move com->itc --- client/src/connection/index.ts | 2 +- client/src/connection/{com => itc}/index.ts | 0 client/src/connection/{com => itc}/script.ts | 0 client/test/connection/{com => itc}/index.test.ts | 6 +++--- 4 files changed, 4 insertions(+), 4 deletions(-) rename client/src/connection/{com => itc}/index.ts (100%) rename client/src/connection/{com => itc}/script.ts (100%) rename client/test/connection/{com => itc}/index.test.ts (98%) diff --git a/client/src/connection/index.ts b/client/src/connection/index.ts index 6e522222f..3586e1be5 100644 --- a/client/src/connection/index.ts +++ b/client/src/connection/index.ts @@ -10,7 +10,7 @@ import { toAutoExecLines, } from "../components/profile"; import { extensionContext } from "../node/extension"; -import { ITCProtocol, getSession as getITCSession } from "./com"; +import { ITCProtocol, getSession as getITCSession } from "./itc"; import { Config as RestConfig, getSession as getRestSession } from "./rest"; import { Error2 as ComputeError, diff --git a/client/src/connection/com/index.ts b/client/src/connection/itc/index.ts similarity index 100% rename from client/src/connection/com/index.ts rename to client/src/connection/itc/index.ts diff --git a/client/src/connection/com/script.ts b/client/src/connection/itc/script.ts similarity index 100% rename from client/src/connection/com/script.ts rename to client/src/connection/itc/script.ts diff --git a/client/test/connection/com/index.test.ts b/client/test/connection/itc/index.test.ts similarity index 98% rename from client/test/connection/com/index.test.ts rename to client/test/connection/itc/index.test.ts index c540477ff..1ff8a268c 100644 --- a/client/test/connection/com/index.test.ts +++ b/client/test/connection/itc/index.test.ts @@ -7,12 +7,12 @@ import { join } from "path"; import { SinonSandbox, SinonStub, createSandbox } from "sinon"; import { stubInterface } from "ts-sinon"; -import { ITCProtocol, getSession } from "../../../src/connection/com"; -import { scriptContent } from "../../../src/connection/com/script"; +import { ITCProtocol, getSession } from "../../../src/connection/itc"; +import { scriptContent } from "../../../src/connection/itc/script"; import { Session } from "../../../src/connection/session"; import { extensionContext } from "../../../src/node/extension"; -describe("COM connection", () => { +describe("ITC connection", () => { let sandbox: SinonSandbox; let spawnStub: SinonStub; let stdoutStub: SinonStub; From 834756084bbc047619a059a970d46851b08180cb Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Wed, 1 Nov 2023 10:24:47 -0400 Subject: [PATCH 08/46] add test for fetch file --- client/src/connection/itc/index.ts | 11 ++++++----- client/test/connection/itc/index.test.ts | 10 ++++++++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/client/src/connection/itc/index.ts b/client/src/connection/itc/index.ts index 41e14ed87..582ae9bb6 100644 --- a/client/src/connection/itc/index.ts +++ b/client/src/connection/itc/index.ts @@ -322,11 +322,12 @@ do { `${this._html5FileName}.htm`, ); this._shellProcess.stdin.write( - ` - $filePath = "${resolve(this._workDirectory, this._html5FileName + ".htm")}"\n - $outputFile = "${outputFileUri.fsPath}"\n - $runner.FetchResultsFile($filePath, $outputFile)\n - `, + `$filePath = "${resolve( + this._workDirectory, + this._html5FileName + ".htm", + )}" +$outputFile = "${outputFileUri.fsPath}" +$runner.FetchResultsFile($filePath, $outputFile)\n`, this.onWriteComplete, ); diff --git a/client/test/connection/itc/index.test.ts b/client/test/connection/itc/index.test.ts index 1ff8a268c..cf89066af 100644 --- a/client/test/connection/itc/index.test.ts +++ b/client/test/connection/itc/index.test.ts @@ -118,15 +118,16 @@ describe("ITC connection", () => { describe("run", () => { const html5 = '
'; + const tempHtmlPath = join(__dirname, "sashtml.htm"); beforeEach(async () => { - writeFileSync(join(__dirname, "sashtml.htm"), html5); + writeFileSync(tempHtmlPath, html5); const setupPromise = session.setup(); onDataCallback(Buffer.from(`WORKDIR=/work/dir`)); await setupPromise; }); afterEach(() => { try { - unlinkSync(join(__dirname, "sashtml.htm")); + unlinkSync(tempHtmlPath); } catch (e) { // Intentionally blank } @@ -150,6 +151,11 @@ describe("ITC connection", () => { ); expect(stdinStub.args[13][0]).to.deep.equal(`$runner.Run($code)\n`); + expect(stdinStub.args[14][0]).to + .equal(`$filePath = "/work/dir/sashtml.htm" +$outputFile = "${tempHtmlPath}" +$runner.FetchResultsFile($filePath, $outputFile) +`); }); }); From 217da85fc9ec726ef9e68bf5fd0739f03bab003a Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Wed, 1 Nov 2023 10:37:08 -0400 Subject: [PATCH 09/46] fix tests for windows --- client/test/connection/itc/index.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/test/connection/itc/index.test.ts b/client/test/connection/itc/index.test.ts index cf89066af..e6db4bcbb 100644 --- a/client/test/connection/itc/index.test.ts +++ b/client/test/connection/itc/index.test.ts @@ -152,8 +152,7 @@ describe("ITC connection", () => { expect(stdinStub.args[13][0]).to.deep.equal(`$runner.Run($code)\n`); expect(stdinStub.args[14][0]).to - .equal(`$filePath = "/work/dir/sashtml.htm" -$outputFile = "${tempHtmlPath}" + .contain(`$outputFile = "${tempHtmlPath}" $runner.FetchResultsFile($filePath, $outputFile) `); }); From d4eadde9e8126c5596e7e8a15a765e0882dacba5 Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Wed, 1 Nov 2023 11:52:43 -0400 Subject: [PATCH 10/46] update CHANGELOG / connect-and-run --- CHANGELOG.md | 1 + connect-and-run.md | 69 ++++++++++++++++++++++++++++++---------------- 2 files changed, 47 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b1f0f7b4..14a3c308d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. This is currently supported for Windows only ([#592](https://github.com/sassoftware/vscode-sas-extension/pull/592)) ## [v1.5.0] - 2023-10-27 diff --git a/connect-and-run.md b/connect-and-run.md index 1b5131081..d051677a0 100644 --- a/connect-and-run.md +++ b/connect-and-run.md @@ -15,7 +15,9 @@ The following commands are supported for profiles: | `SAS.updateProfile` | SAS: Update Connection profile | | `SAS.deleteProfile` | SAS: Delete Connection profile | -## Profile Anatomy (SAS Viya) +## Profile: SAS Viya + +### Profile Anatomy The parameters listed below make up the profile settings for configuring a connection to SAS Viya. @@ -36,10 +38,26 @@ Open the command palette (`F1`, or `Ctrl+Shift+P` on Windows or Linux, or `Shift For more information about Client IDs and the authentication process, please see the blog post [Authentication to SAS Viya: a couple of approaches](https://blogs.sas.com/content/sgf/2021/09/24/authentication-to-sas-viya/). A SAS administrator can follow the Steps 1 and 2 in the post to register a new client. -## Profile Anatomy (SAS 9.4 Remote) +## Profile: SAS 9.4 (remote - SSH) For a secure connection to SAS 9.4 remote server, a public / private ssh key pair is required. The socket defined in the environment variable `SSH_AUTH_SOCK` is used to communicate with ssh-agent to authenticate the ssh session. The private key must be registered with the ssh-agent. The steps for configuring ssh follow. +### Profile Anatomy + +The parameters listed below make up the profile settings for configuring a connection to a remote SAS 9.4 instance. + +| Name | Description | Additional Notes | +| ------------ | ------------------------------------ | -------------------------------------------------------------------- | +| **Name** | Name of the profile | This will display on the status bar | +| **Host** | SSH Server Host | This will appear when hovering over the status bar | +| **Username** | SSH Server Username | A username to use when establishing the SSH connection to the server | +| **Port** | SSH Server Port | The ssh port of the SSH server. Default value is 22 | +| **SAS Path** | Path to SAS Executable on the server | Must be a fully qualified path on the SSH server to a SAS executable | + +### Add New SAS 9.4 (remote - SSH) Profile + +Open the command palette (`F1`, or `Ctrl+Shift+P` on Windows or Linux, or `Shift+CMD+P` on OSX). After executing the `SAS.addProfile` command, select the SAS 9.4 (remote - SSH) connection type and complete the prompts (using values from the preceeding table) to create a new profile. + ### Required setup for connection to SAS 9.4 In order to configure the connection between VS Code and SAS 9, you must configure OpenSSH. Follow the steps below to complete the setup. @@ -48,7 +66,7 @@ In order to configure the connection between VS Code and SAS 9, you must configu 1. Enable openssh client optional feature; [instructions found here](https://learn.microsoft.com/en-us/windows-server/administration/openssh/openssh_install_firstuse?tabs=gui). -2. [Create an environment variable](https://phoenixnap.com/kb/windows-set-environment-variable) SSH_AUTH_SOCK with value //./pipe/openssh-ssh-agent (windows uses a named pipe for the auth sock). +2. [Create an environment variable](https://phoenixnap.com/kb/windows-set-environment-variable) SSH_AUTH_SOCK with value //./pipe/openssh-ssh-agent (windows uses a named pipe for the auth sock). **Note**: An attempt to create the varible using Powershell command line did not register; suggest using these GUI instructions. 3. Ensure ssh-agent service is running and set startup type to automatic; commands found in [this link](https://dev.to/aka_anoop/how-to-enable-openssh-agent-to-access-your-github-repositories-on-windows-powershell-1ab8) @@ -114,7 +132,7 @@ Note: the default path to the SAS executable (saspath) is /opt/sasinside/SASHome 4. Add the private key to ssh-agent: ssh-add /path/to/private/key/with/passphrase -5. Define a connection profile in settings.json for a remote server (see detailed instructions below in the [Add New SAS 9.4 Remote Profile](#add-new-sas-94-remote-profile) section): +5. Define a connection profile in settings.json for a remote server (see detailed instructions below in the [Add New SAS 9.4 Remote (Via SSH) Profile](#add-new-sas-94-remote-via-ssh-profile) section): ``` "ssh_test": { @@ -128,26 +146,12 @@ Note: the default path to the SAS executable (saspath) is /opt/sasinside/SASHome 6. Add the public part of the keypair to the SAS server. Add the contents of the key file to the ~/.ssh/authorized_keys file. -### Profile Anatomy (SAS 9.4 Remote) - -The parameters listed below make up the profile settings for configuring a connection to a remote SAS 9.4 instance. - -| Name | Description | Additional Notes | -| ------------ | ------------------------------------ | -------------------------------------------------------------------- | -| **Name** | Name of the profile | This will display on the status bar | -| **Host** | SSH Server Host | This will appear when hovering over the status bar | -| **Username** | SSH Server Username | A username to use when establishing the SSH connection to the server | -| **Port** | SSH Server Port | The ssh port of the SSH server. Default value is 22 | -| **SAS Path** | Path to SAS Executable on the server | Must be a fully qualified path on the SSH server to a SAS executable | - -## Add New SAS 9.4 Remote Profile - -Open the command palette (`F1`, or `Ctrl+Shift+P` on Windows or Linux, or `Shift+CMD+P` on OSX). After executing the `SAS.addProfile` command, select the SAS 9.4 (remote) connection type and complete the prompts (using values from the preceeding table) to create a new profile. - -## Profile Anatomy (SAS 9.4 local) +## Profile: SAS 9.4 (local) On Windows, during the install of SAS 9.4, make sure that "Integration Technologies Client" checkbox is checked. If using an order that doesn't provide this option, make sure the ITC is installed by visiting the following [link](https://support.sas.com/downloads/browse.htm?fil=&cat=56). Make sure to download the 9.4m8 option. +### Profile Anatomy + The parameters listed below make up the profile settings for configuring a connection to a local SAS 9.4 instance. | Name | Description | Additional Notes | @@ -155,9 +159,28 @@ The parameters listed below make up the profile settings for configuring a conne | **Name** | Name of the profile | This will display on the status bar | | **Host** | Indicates SAS 9.4 local instance | Defaults to localhost for com | -## Add New SAS 9.4 Local Profile +### Add New SAS 9.4 (local) Profile + +Open the command palette (`F1`, or `Ctrl+Shift+P` on Windows). After executing the `SAS.addProfile` command, select the SAS 9.4 (local) connection type to create a new profile. + +## Profile: SAS 9.4 (remote - IOM) + +On Windows, during the install of SAS 9.4, make sure that "Integration Technologies Client" checkbox is checked. If using an order that doesn't provide this option, make sure the ITC is installed by visiting the following [link](https://support.sas.com/downloads/browse.htm?fil=&cat=56). Make sure to download the 9.4m8 option. + +### Profile Anatomy + +The parameters listed below make up the profile settings for configuring a connection to a remote SAS 9.4 instance. + +| Name | Description | Additional Notes | +| ------------ | ------------------- | -------------------------------------------------------------------- | +| **Name** | Name of the profile | This will display on the status bar | +| **Host** | IOM Server Host | This will appear when hovering over the status bar | +| **Username** | IOM Server Username | A username to use when establishing the IOM connection to the server | +| **Port** | IOM Server Port | The port of the IOM server. Default value is 8591 | + +### Add New SAS 9.4 (remote - IOM) Profile -Open the command palette (`F1`, or `Ctrl+Shift+P` on Windows). After executing the `SAS.addProfile` command, select the SAS 9.4 (local - COM) connection type to create a new profile. +Open the command palette (`F1`, or `Ctrl+Shift+P` on Windows). After executing the `SAS.addProfile` command, select the SAS 9.4 (remote - IOM) connection type to create a new profile. ## Additional settings in a profile From 0f445a00951afe68ec5555e0db298c5822a54ad6 Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Wed, 1 Nov 2023 11:58:06 -0400 Subject: [PATCH 11/46] Update README --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e5538a6d0..98b659de2 100644 --- a/README.md +++ b/README.md @@ -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) From 046459ab03997936db13f08254003f8bb4dd0c33 Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Wed, 1 Nov 2023 12:01:09 -0400 Subject: [PATCH 12/46] Run npm run format --- client/test/connection/itc/index.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/test/connection/itc/index.test.ts b/client/test/connection/itc/index.test.ts index e6db4bcbb..d4933dc74 100644 --- a/client/test/connection/itc/index.test.ts +++ b/client/test/connection/itc/index.test.ts @@ -151,8 +151,7 @@ describe("ITC connection", () => { ); expect(stdinStub.args[13][0]).to.deep.equal(`$runner.Run($code)\n`); - expect(stdinStub.args[14][0]).to - .contain(`$outputFile = "${tempHtmlPath}" + expect(stdinStub.args[14][0]).to.contain(`$outputFile = "${tempHtmlPath}" $runner.FetchResultsFile($filePath, $outputFile) `); }); From 0b5919ff4dfad5004f80ee33e9207e6a456697c6 Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Wed, 1 Nov 2023 12:09:38 -0400 Subject: [PATCH 13/46] Rename COMSession -> ITCSession --- client/src/connection/itc/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/connection/itc/index.ts b/client/src/connection/itc/index.ts index 582ae9bb6..d3d363536 100644 --- a/client/src/connection/itc/index.ts +++ b/client/src/connection/itc/index.ts @@ -13,7 +13,7 @@ import { scriptContent } from "./script"; const PASSWORD_KEY = "ITC_PASSWORD_KEY"; const endCode = "--vscode-sas-extension-submit-end--"; -let sessionInstance: COMSession; +let sessionInstance: ITCSession; export enum ITCProtocol { COM = 0, @@ -30,7 +30,7 @@ export interface Config extends BaseConfig { protocol: ITCProtocol; } -export class COMSession extends Session { +export class ITCSession extends Session { private _config: Config; private _shellProcess: ChildProcessWithoutNullStreams; private _html5FileName: string; @@ -381,7 +381,7 @@ export const getSession = ( }; if (!sessionInstance) { - sessionInstance = new COMSession(extensionContext); + sessionInstance = new ITCSession(extensionContext); } sessionInstance.config = { ...defaults, ...c }; return sessionInstance; From c3474bf8a06d8d363706a50a5b2fc2d1d4fc7a73 Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Fri, 3 Nov 2023 13:09:07 -0400 Subject: [PATCH 14/46] Update const/comment/default port --- client/src/components/profile.ts | 10 +++++----- client/src/connection/itc/index.ts | 4 ++-- package.json | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/client/src/components/profile.ts b/client/src/components/profile.ts index c7f385183..f7cde8453 100644 --- a/client/src/components/profile.ts +++ b/client/src/components/profile.ts @@ -17,15 +17,15 @@ export const EXTENSION_PROFILES_CONFIG_KEY = "profiles"; export const EXTENSION_ACTIVE_PROFILE_CONFIG_KEY = "activeProfile"; enum ConnectionOptions { - SASViya = "SAS Viya", - SAS94Remote = "SAS 9.4 (remote - SSH)", - SAS9IOM = "SAS 9.4 (remote - IOM)", SAS9COM = "SAS 9.4 (local)", + SAS9IOM = "SAS 9.4 (remote - IOM)", + SAS9SSH = "SAS 9.4 (remote - SSH)", + SASViya = "SAS Viya", } const CONNECTION_PICK_OPTS: string[] = [ ConnectionOptions.SASViya, - ConnectionOptions.SAS94Remote, + ConnectionOptions.SAS9SSH, ConnectionOptions.SAS9IOM, ConnectionOptions.SAS9COM, ]; @@ -810,7 +810,7 @@ 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; diff --git a/client/src/connection/itc/index.ts b/client/src/connection/itc/index.ts index d3d363536..5d7b016b0 100644 --- a/client/src/connection/itc/index.ts +++ b/client/src/connection/itc/index.ts @@ -189,7 +189,7 @@ export class ITCSession extends Session { }; /** - * Cleans up resources for the given local SAS session. + * Cleans up resources for the given SAS session. * @returns void promise. */ public close = async (): Promise => { @@ -364,7 +364,7 @@ $runner.FetchResultsFile($filePath, $outputFile)\n`, } /** - * Creates a new SAS 9 Local Session. + * Creates a new SAS 9 Session. * @param c Instance denoting configuration parameters for this connection profile. * @returns created COM session. */ diff --git a/package.json b/package.json index 75f9a7964..1805c1165 100644 --- a/package.json +++ b/package.json @@ -351,7 +351,7 @@ }, "port": { "type": "number", - "default": 22, + "default": 8591, "description": "%configuration.SAS.connectionProfiles.profiles.ssh.port%", "exclusiveMinimum": 1, "exclusiveMaximum": 65535 From 7cf22555561afba6591582cffab6eacaead42a3b Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Fri, 3 Nov 2023 13:17:15 -0400 Subject: [PATCH 15/46] Update connect-and-run --- connect-and-run.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connect-and-run.md b/connect-and-run.md index d051677a0..8a3048607 100644 --- a/connect-and-run.md +++ b/connect-and-run.md @@ -165,7 +165,7 @@ Open the command palette (`F1`, or `Ctrl+Shift+P` on Windows). After executing t ## Profile: SAS 9.4 (remote - IOM) -On Windows, during the install of SAS 9.4, make sure that "Integration Technologies Client" checkbox is checked. If using an order that doesn't provide this option, make sure the ITC is installed by visiting the following [link](https://support.sas.com/downloads/browse.htm?fil=&cat=56). Make sure to download the 9.4m8 option. +In order to use this option, you'll need to have "Integration Technologies Client" (ITC) installed. You can do this by making sure the "Integration Technologies Client" checkbox is checked when installing SAS 9.4, or by visiting the following [link](https://support.sas.com/downloads/browse.htm?fil=&cat=56) (Make sure to download the 9.4m8 option). ### Profile Anatomy From 16a03094bcf2ee5ae130d72c6ae982005680461e Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Fri, 3 Nov 2023 13:34:00 -0400 Subject: [PATCH 16/46] Use components/ExtensionContext --- client/src/components/ExtensionContext.ts | 14 ++++++++++- client/src/connection/index.ts | 13 ++-------- client/src/connection/itc/index.ts | 30 ++++++++++++----------- client/test/connection/itc/index.test.ts | 5 +++- 4 files changed, 35 insertions(+), 27 deletions(-) diff --git a/client/src/components/ExtensionContext.ts b/client/src/components/ExtensionContext.ts index 1fd52e836..2e209008f 100644 --- a/client/src/components/ExtensionContext.ts +++ b/client/src/components/ExtensionContext.ts @@ -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; @@ -26,3 +26,15 @@ export async function getContextValue( ): Promise { return context.workspaceState.get(key); } + +export async function setSecret(key: string, value: string): Promise { + await context.secrets.store(key, value); +} + +export async function getSecret(key: string): Promise { + return await context.secrets.get(key); +} + +export function getGlobalStorageUri(): Uri { + return context.globalStorageUri; +} diff --git a/client/src/connection/index.ts b/client/src/connection/index.ts index 3586e1be5..c7dcc4372 100644 --- a/client/src/connection/index.ts +++ b/client/src/connection/index.ts @@ -9,7 +9,6 @@ import { ViyaProfile, toAutoExecLines, } from "../components/profile"; -import { extensionContext } from "../node/extension"; import { ITCProtocol, getSession as getITCSession } from "./itc"; import { Config as RestConfig, getSession as getRestSession } from "./rest"; import { @@ -55,17 +54,9 @@ export function getSession(): Session { case ConnectionType.SSH: return getSSHSession(validProfile.profile); case ConnectionType.COM: - return getITCSession( - validProfile.profile, - ITCProtocol.COM, - extensionContext, - ); + return getITCSession(validProfile.profile, ITCProtocol.COM); case ConnectionType.IOM: - return getITCSession( - validProfile.profile, - ITCProtocol.IOMBridge, - extensionContext, - ); + return getITCSession(validProfile.profile, ITCProtocol.IOMBridge); default: throw new Error( l10n.t("Invalid connectionType. Check Profile settings."), diff --git a/client/src/connection/itc/index.ts b/client/src/connection/itc/index.ts index 5d7b016b0..4eebc8a74 100644 --- a/client/src/connection/itc/index.ts +++ b/client/src/connection/itc/index.ts @@ -1,11 +1,16 @@ // Copyright © 2023, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { ExtensionContext, Uri, l10n, window, workspace } from "vscode"; +import { Uri, l10n, window, workspace } from "vscode"; import { ChildProcessWithoutNullStreams, spawn } from "child_process"; import { resolve } from "path"; import { BaseConfig, RunResult } from ".."; +import { + getGlobalStorageUri, + getSecret, + setSecret, +} from "../../components/ExtensionContext"; import { updateStatusBarItem } from "../../components/StatusBarItem"; import { Session } from "../session"; import { scriptContent } from "./script"; @@ -38,12 +43,10 @@ export class ITCSession extends Session { private _runReject: ((reason?) => void) | undefined; private _workDirectory: string; private _password: string; - private _context: ExtensionContext; - constructor(extensionContext: ExtensionContext) { + constructor() { super(); this._password = ""; - this._context = extensionContext; } public set config(value: Config) { @@ -124,12 +127,11 @@ export class ITCSession extends Session { return setupPromise; }; - private storePassword = async () => { - await this._context.secrets.store(PASSWORD_KEY, this._password); - }; + private storePassword = async () => + await setSecret(PASSWORD_KEY, this._password); private clearPassword = async () => { - await this._context.secrets.delete(PASSWORD_KEY); + await setSecret(PASSWORD_KEY, ""); this._password = ""; }; @@ -138,7 +140,7 @@ export class ITCSession extends Session { return ""; } - const storedPassword = await this._context.secrets.get(PASSWORD_KEY); + const storedPassword = await getSecret(PASSWORD_KEY); if (storedPassword) { return storedPassword; } @@ -311,14 +313,15 @@ do { * Fetches the ODS output results for the latest html results file. */ private fetchResults = async () => { + const globalStorageUri = getGlobalStorageUri(); try { - await workspace.fs.readDirectory(this._context.globalStorageUri); + await workspace.fs.readDirectory(globalStorageUri); } catch (e) { - await workspace.fs.createDirectory(this._context.globalStorageUri); + await workspace.fs.createDirectory(globalStorageUri); } const outputFileUri = Uri.joinPath( - this._context.globalStorageUri, + globalStorageUri, `${this._html5FileName}.htm`, ); this._shellProcess.stdin.write( @@ -371,7 +374,6 @@ $runner.FetchResultsFile($filePath, $outputFile)\n`, export const getSession = ( c: Partial, protocol: ITCProtocol, - extensionContext: ExtensionContext, ): Session => { const defaults = { host: "localhost", @@ -381,7 +383,7 @@ export const getSession = ( }; if (!sessionInstance) { - sessionInstance = new ITCSession(extensionContext); + sessionInstance = new ITCSession(); } sessionInstance.config = { ...defaults, ...c }; return sessionInstance; diff --git a/client/test/connection/itc/index.test.ts b/client/test/connection/itc/index.test.ts index d4933dc74..7161340be 100644 --- a/client/test/connection/itc/index.test.ts +++ b/client/test/connection/itc/index.test.ts @@ -7,6 +7,7 @@ import { join } from "path"; import { SinonSandbox, SinonStub, createSandbox } from "sinon"; import { stubInterface } from "ts-sinon"; +import { setContext } from "../../../src/components/ExtensionContext"; import { ITCProtocol, getSession } from "../../../src/connection/itc"; import { scriptContent } from "../../../src/connection/itc/script"; import { Session } from "../../../src/connection/session"; @@ -62,7 +63,9 @@ describe("ITC connection", () => { secrets: secretStore, }; - session = getSession(config, ITCProtocol.COM, stubbedExtensionContext); + setContext(stubbedExtensionContext); + + session = getSession(config, ITCProtocol.COM); session.onLogFn = () => { return; }; From 591357b3920fc58e7908f77efe09b872745cb6a9 Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Fri, 10 Nov 2023 09:40:25 -0500 Subject: [PATCH 17/46] store password when fetched from session --- client/src/connection/itc/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/connection/itc/index.ts b/client/src/connection/itc/index.ts index 4eebc8a74..100a1ed42 100644 --- a/client/src/connection/itc/index.ts +++ b/client/src/connection/itc/index.ts @@ -142,6 +142,7 @@ export class ITCSession extends Session { const storedPassword = await getSecret(PASSWORD_KEY); if (storedPassword) { + this._password = storedPassword; return storedPassword; } From 4fd49ff7e2f73cd75c8d6eae86ce20c136b2ab56 Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Fri, 10 Nov 2023 10:43:09 -0500 Subject: [PATCH 18/46] introduce namespaced secret storage --- client/src/components/AuthProvider.ts | 34 +++++-------------- client/src/components/ExtensionContext.ts | 40 +++++++++++++++++++---- client/src/connection/itc/index.ts | 15 +++++---- client/src/node/extension.ts | 2 +- 4 files changed, 52 insertions(+), 39 deletions(-) diff --git a/client/src/components/AuthProvider.ts b/client/src/components/AuthProvider.ts index 538c8f0e2..c9cb12503 100644 --- a/client/src/components/AuthProvider.ts +++ b/client/src/components/AuthProvider.ts @@ -8,7 +8,6 @@ import { Disposable, Event, EventEmitter, - SecretStorage, commands, workspace, } from "vscode"; @@ -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"; @@ -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 = @@ -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) => { @@ -52,6 +53,7 @@ export class SASAuthProvider implements AuthenticationProvider, Disposable { } }), ]; + this.secretStorage = getSecretStorage(SECRET_KEY); } dispose(): void { @@ -80,7 +82,7 @@ export class SASAuthProvider implements AuthenticationProvider, Disposable { } private async _getSessions(): Promise { - const sessions = await this.getStoredSessions(); + const sessions = await this.secretStorage.getNamespaceData(); if (!sessions) { return []; } @@ -152,18 +154,11 @@ export class SASAuthProvider implements AuthenticationProvider, Disposable { } private async writeSession(session: SASAuthSession): Promise { - 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 { - const sessions = await this.getStoredSessions(); + const sessions = await this.secretStorage.getNamespaceData(); if (!sessions) { return; } @@ -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({ @@ -198,15 +193,4 @@ export class SASAuthProvider implements AuthenticationProvider, Disposable { commands.executeCommand("setContext", "SAS.authorized", false); } } - - private async getStoredSessions(): Promise< - Record | undefined - > { - const storedSessionData = await this.secretStorage.get(SECRET_KEY); - if (!storedSessionData) { - return; - } - - return JSON.parse(storedSessionData); - } } diff --git a/client/src/components/ExtensionContext.ts b/client/src/components/ExtensionContext.ts index 2e209008f..360ad0d70 100644 --- a/client/src/components/ExtensionContext.ts +++ b/client/src/components/ExtensionContext.ts @@ -27,14 +27,40 @@ export async function getContextValue( return context.workspaceState.get(key); } -export async function setSecret(key: string, value: string): Promise { - await context.secrets.store(key, value); +export function getGlobalStorageUri(): Uri { + return context.globalStorageUri; } -export async function getSecret(key: string): Promise { - return await context.secrets.get(key); -} +export function getSecretStorage(namespace: string) { + const getNamespaceData = async (): Promise | undefined> => { + const storedSessionData = await context.secrets.get(namespace); + if (!storedSessionData) { + return; + } -export function getGlobalStorageUri(): Uri { - return context.globalStorageUri; + return JSON.parse(storedSessionData); + }; + const setNamespaceData = async (data: Record) => { + await context.secrets.store(namespace, JSON.stringify(data)); + }; + + const get = async (key: string): Promise => { + 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 }; } diff --git a/client/src/connection/itc/index.ts b/client/src/connection/itc/index.ts index 100a1ed42..d2e64d9d3 100644 --- a/client/src/connection/itc/index.ts +++ b/client/src/connection/itc/index.ts @@ -8,14 +8,13 @@ import { resolve } from "path"; import { BaseConfig, RunResult } from ".."; import { getGlobalStorageUri, - getSecret, - setSecret, + getSecretStorage, } from "../../components/ExtensionContext"; import { updateStatusBarItem } from "../../components/StatusBarItem"; import { Session } from "../session"; import { scriptContent } from "./script"; -const PASSWORD_KEY = "ITC_PASSWORD_KEY"; +const SECRET_STORAGE_NAMESPACE = "ITC_SECRET_STORAGE"; const endCode = "--vscode-sas-extension-submit-end--"; let sessionInstance: ITCSession; @@ -43,14 +42,18 @@ export class ITCSession extends Session { private _runReject: ((reason?) => void) | undefined; private _workDirectory: string; private _password: string; + private _secretStorage; + private _passwordKey: string; constructor() { super(); this._password = ""; + this._secretStorage = getSecretStorage(SECRET_STORAGE_NAMESPACE); } public set config(value: Config) { this._config = value; + this._passwordKey = `${value.host}${value.protocol}${value.username}`; } /** @@ -128,10 +131,10 @@ export class ITCSession extends Session { }; private storePassword = async () => - await setSecret(PASSWORD_KEY, this._password); + await this._secretStorage.store(this._passwordKey, this._password); private clearPassword = async () => { - await setSecret(PASSWORD_KEY, ""); + await this._secretStorage.store(this._passwordKey, ""); this._password = ""; }; @@ -140,7 +143,7 @@ export class ITCSession extends Session { return ""; } - const storedPassword = await getSecret(PASSWORD_KEY); + const storedPassword = await this._secretStorage.get(this._passwordKey); if (storedPassword) { this._password = storedPassword; return storedPassword; diff --git a/client/src/node/extension.ts b/client/src/node/extension.ts index baf1f1e60..74c99a4ed 100644 --- a/client/src/node/extension.ts +++ b/client/src/node/extension.ts @@ -128,7 +128,7 @@ export function activate(context: ExtensionContext): void { authentication.registerAuthenticationProvider( SASAuthProvider.id, "SAS", - new SASAuthProvider(context.secrets), + new SASAuthProvider(), ), languages.registerDocumentSemanticTokensProvider( { language: "sas-log" }, From 5efd0ecbd4805a4b939101417c95ac9e789e19a7 Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Fri, 10 Nov 2023 11:23:09 -0500 Subject: [PATCH 19/46] update how sessions are stored/results are fetched --- client/src/connection/itc/index.ts | 54 ++++++++++++++---------- client/src/connection/itc/script.ts | 4 ++ client/src/connection/itc/types.ts | 8 ++++ client/test/connection/itc/index.test.ts | 4 +- 4 files changed, 46 insertions(+), 24 deletions(-) create mode 100644 client/src/connection/itc/types.ts diff --git a/client/src/connection/itc/index.ts b/client/src/connection/itc/index.ts index d2e64d9d3..464e14ab9 100644 --- a/client/src/connection/itc/index.ts +++ b/client/src/connection/itc/index.ts @@ -13,10 +13,10 @@ import { import { updateStatusBarItem } from "../../components/StatusBarItem"; import { Session } from "../session"; import { scriptContent } from "./script"; +import { LineCodes } from "./types"; const SECRET_STORAGE_NAMESPACE = "ITC_SECRET_STORAGE"; -const endCode = "--vscode-sas-extension-submit-end--"; let sessionInstance: ITCSession; export enum ITCProtocol { @@ -179,7 +179,7 @@ export class ITCSession extends Session { ); //write an end mnemonic so that the handler knows when execution has finished - const codeWithEnd = `${codeWithODSPath}\n%put ${endCode};`; + const codeWithEnd = `${codeWithODSPath}\n%put ${LineCodes.RunEndCode};`; const codeToRun = `$code=\n@'\n${codeWithEnd}\n'@\n`; this._shellProcess.stdin.write(codeToRun); @@ -284,10 +284,7 @@ do { updateStatusBarItem(true); return; } - if (line.endsWith(endCode)) { - // run completed - this.fetchResults(); - } else { + if (!this.processLineCodes(line)) { this._html5FileName = line.match(/NOTE: .+ HTML5.* Body.+: (.+)\.htm/)?.[1] ?? this._html5FileName; @@ -296,6 +293,26 @@ do { }); }; + private processLineCodes(line: string): boolean { + if (line.endsWith(LineCodes.RunEndCode)) { + // run completed + this.fetchResults(); + return true; + } + + if (line.includes(LineCodes.SessionCreatedCode)) { + this.storePassword(); + return true; + } + + if (line.includes(LineCodes.ResultsFetchedCode)) { + this.displayResults(); + return true; + } + + return false; + } + /** * Generic call for use on stdin write completion. * @param err The error encountered on the write attempt. Undefined if no error occurred. @@ -337,23 +354,15 @@ $outputFile = "${outputFileUri.fsPath}" $runner.FetchResultsFile($filePath, $outputFile)\n`, this.onWriteComplete, ); + }; - const file = await new Promise((resolve) => { - const start = Date.now(); - const maxTime = 10 * 1000; - const interval = setInterval(async () => { - try { - const file = await workspace.fs.readFile(outputFileUri); - clearInterval(interval); - resolve(file); - } catch (e) { - if (Date.now() - maxTime > start) { - clearInterval(interval); - resolve(null); - } - } - }, 1000); - }); + private displayResults = async () => { + const globalStorageUri = getGlobalStorageUri(); + const outputFileUri = Uri.joinPath( + globalStorageUri, + `${this._html5FileName}.htm`, + ); + const file = await workspace.fs.readFile(outputFileUri); const htmlResults = (file || "").toString(); if (file) { @@ -365,7 +374,6 @@ $runner.FetchResultsFile($filePath, $outputFile)\n`, runResult.html5 = htmlResults; runResult.title = "Result"; } - this.storePassword(); this._runResolve(runResult); }; } diff --git a/client/src/connection/itc/script.ts b/client/src/connection/itc/script.ts index b17593689..8d53a5298 100644 --- a/client/src/connection/itc/script.ts +++ b/client/src/connection/itc/script.ts @@ -1,5 +1,6 @@ // Copyright © 2023, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { LineCodes } from "./types"; export const scriptContent = ` class SASRunner{ @@ -38,6 +39,7 @@ class SASRunner{ $password ) + Write-Host "${LineCodes.SessionCreatedCode}" } catch { throw "Setup error" } @@ -127,6 +129,8 @@ class SASRunner{ $objStream.Close() $outStream.Close() $this.objSAS.FileService.DeassignFileref($objFile.FilerefName) + + Write-Host "${LineCodes.ResultsFetchedCode}" } } `; diff --git a/client/src/connection/itc/types.ts b/client/src/connection/itc/types.ts new file mode 100644 index 000000000..914f3d0a6 --- /dev/null +++ b/client/src/connection/itc/types.ts @@ -0,0 +1,8 @@ +// Copyright © 2023, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +export enum LineCodes { + ResultsFetchedCode = "--vscode-sas-extension-results-fetched--", + RunEndCode = "--vscode-sas-extension-submit-end--", + SessionCreatedCode = "--vscode-sas-extension-session-created--", +} diff --git a/client/test/connection/itc/index.test.ts b/client/test/connection/itc/index.test.ts index 7161340be..75833cbce 100644 --- a/client/test/connection/itc/index.test.ts +++ b/client/test/connection/itc/index.test.ts @@ -10,6 +10,7 @@ import { stubInterface } from "ts-sinon"; import { setContext } from "../../../src/components/ExtensionContext"; import { ITCProtocol, getSession } from "../../../src/connection/itc"; import { scriptContent } from "../../../src/connection/itc/script"; +import { LineCodes } from "../../../src/connection/itc/types"; import { Session } from "../../../src/connection/session"; import { extensionContext } from "../../../src/node/extension"; @@ -143,7 +144,8 @@ describe("ITC connection", () => { //simulate log message for body file onDataCallback(Buffer.from("NOTE: Writing HTML5 Body file: sashtml.htm")); //simulate end of submission - onDataCallback(Buffer.from("--vscode-sas-extension-submit-end--")); + onDataCallback(Buffer.from(LineCodes.RunEndCode)); + onDataCallback(Buffer.from(LineCodes.ResultsFetchedCode)); const runResult = await runPromise; expect(runResult.html5).to.equal(html5); From 5ec2211fb6c579ce0a3f8fd02ec676efb54d8dfa Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Fri, 10 Nov 2023 15:22:07 -0500 Subject: [PATCH 20/46] wip - implement cancel --- client/src/connection/itc/index.ts | 18 +++++++++++++++++- client/src/connection/itc/script.ts | 14 ++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/client/src/connection/itc/index.ts b/client/src/connection/itc/index.ts index 464e14ab9..15bf01a92 100644 --- a/client/src/connection/itc/index.ts +++ b/client/src/connection/itc/index.ts @@ -217,6 +217,22 @@ export class ITCSession extends Session { }); }; + /** + * Cancels a running SAS program + */ + public cancel = async () => { + this._shellProcess.stdin.write( + "$runner.Cancel()\n", + async (error) => { + if (error) { + this._runReject(error); + } + + await this.fetchLog(); + } + ); + }; + /** * Formats the SAS Options provided in the profile into a format * that the shell process can understand. @@ -294,7 +310,7 @@ do { }; private processLineCodes(line: string): boolean { - if (line.endsWith(LineCodes.RunEndCode)) { + if (line.includes(LineCodes.RunEndCode)) { // run completed this.fetchResults(); return true; diff --git a/client/src/connection/itc/script.ts b/client/src/connection/itc/script.ts index 8d53a5298..bc6dbbad8 100644 --- a/client/src/connection/itc/script.ts +++ b/client/src/connection/itc/script.ts @@ -77,6 +77,8 @@ class SASRunner{ [void]Run([string]$code) { try{ + $this.objSAS.LanguageService.Reset() + $this.objSAS.LanguageService.Async = $true $this.objSAS.LanguageService.Submit($code) }catch{ Write-Error $_.ScriptStackTrace @@ -84,6 +86,10 @@ class SASRunner{ } } + [void]SubmitComplete() { + Write-Host "Yayyyyyy" + } + [void]Close(){ try{ $this.objSAS.Close() @@ -92,6 +98,14 @@ class SASRunner{ } } + [void]Cancel(){ + try{ + $this.objSAS.LanguageService.Cancel() + }catch{ + throw "Cancel error" + } + } + [String]FlushLog([int]$chunkSize) { try{ return $this.objSAS.LanguageService.FlushLog($chunkSize) From aff5798f9450397191b0c77f9258dd32ad43d071 Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Wed, 15 Nov 2023 11:58:45 -0500 Subject: [PATCH 21/46] clear password on session close --- client/src/connection/itc/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/connection/itc/index.ts b/client/src/connection/itc/index.ts index 15bf01a92..a4e2e767f 100644 --- a/client/src/connection/itc/index.ts +++ b/client/src/connection/itc/index.ts @@ -212,6 +212,7 @@ export class ITCSession extends Session { this._runReject = undefined; this._runResolve = undefined; } + this.clearPassword(); resolve(); updateStatusBarItem(false); }); @@ -310,7 +311,7 @@ do { }; private processLineCodes(line: string): boolean { - if (line.includes(LineCodes.RunEndCode)) { + if (line.endsWith(LineCodes.RunEndCode)) { // run completed this.fetchResults(); return true; From cf93f62cc1848f12905a88a761add56a06b76a69 Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Fri, 10 Nov 2023 15:22:07 -0500 Subject: [PATCH 22/46] implement cancel --- client/src/connection/itc/index.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/client/src/connection/itc/index.ts b/client/src/connection/itc/index.ts index a4e2e767f..16e9968ed 100644 --- a/client/src/connection/itc/index.ts +++ b/client/src/connection/itc/index.ts @@ -222,16 +222,13 @@ export class ITCSession extends Session { * Cancels a running SAS program */ public cancel = async () => { - this._shellProcess.stdin.write( - "$runner.Cancel()\n", - async (error) => { - if (error) { - this._runReject(error); - } - - await this.fetchLog(); + this._shellProcess.stdin.write("$runner.Cancel()\n", async (error) => { + if (error) { + this._runReject(error); } - ); + + await this.fetchLog(); + }); }; /** @@ -311,7 +308,7 @@ do { }; private processLineCodes(line: string): boolean { - if (line.endsWith(LineCodes.RunEndCode)) { + if (line.includes(LineCodes.RunEndCode)) { // run completed this.fetchResults(); return true; From 4473702599a0413fa11e4e6ec2b663d3b52c0232 Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Wed, 15 Nov 2023 12:07:28 -0500 Subject: [PATCH 23/46] Update documentation/npm run format --- CHANGELOG.md | 2 +- connect-and-run.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14a3c308d..4ea55e1f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +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. This is currently supported for Windows only ([#592](https://github.com/sassoftware/vscode-sas-extension/pull/592)) +- 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 diff --git a/connect-and-run.md b/connect-and-run.md index 8a3048607..bb731b498 100644 --- a/connect-and-run.md +++ b/connect-and-run.md @@ -165,7 +165,7 @@ Open the command palette (`F1`, or `Ctrl+Shift+P` on Windows). After executing t ## Profile: SAS 9.4 (remote - IOM) -In order to use this option, you'll need to have "Integration Technologies Client" (ITC) installed. You can do this by making sure the "Integration Technologies Client" checkbox is checked when installing SAS 9.4, or by visiting the following [link](https://support.sas.com/downloads/browse.htm?fil=&cat=56) (Make sure to download the 9.4m8 option). +In order to use this option, you'll need to have "Integration Technologies Client" (ITC) installed on the machine this extension runs on. You can do this by making sure the "Integration Technologies Client" checkbox is checked when installing SAS 9.4, or by visiting the following [link](https://support.sas.com/downloads/browse.htm?fil=&cat=56) (Make sure to download the 9.4m8 option). ### Profile Anatomy From a091f7ced2ef038f1a99921138f1866d14f8df97 Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Fri, 17 Nov 2023 13:30:58 -0500 Subject: [PATCH 24/46] cleanup testing method --- client/src/connection/itc/script.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/client/src/connection/itc/script.ts b/client/src/connection/itc/script.ts index bc6dbbad8..f3ba2a6cb 100644 --- a/client/src/connection/itc/script.ts +++ b/client/src/connection/itc/script.ts @@ -86,10 +86,6 @@ class SASRunner{ } } - [void]SubmitComplete() { - Write-Host "Yayyyyyy" - } - [void]Close(){ try{ $this.objSAS.Close() From c740bd64ecb7a9166629801c235f874a5598057a Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Mon, 20 Nov 2023 13:25:50 -0500 Subject: [PATCH 25/46] update translations --- package.json | 6 +++--- package.nls.json | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 1805c1165..1455020dd 100644 --- a/package.json +++ b/package.json @@ -342,17 +342,17 @@ "host": { "type": "string", "default": "", - "description": "%configuration.SAS.connectionProfiles.profiles.ssh.host%" + "description": "%configuration.SAS.connectionProfiles.profiles.iom.host%" }, "username": { "type": "string", "default": "", - "description": "%configuration.SAS.connectionProfiles.profiles.ssh.username%" + "description": "%configuration.SAS.connectionProfiles.profiles.iom.username%" }, "port": { "type": "number", "default": 8591, - "description": "%configuration.SAS.connectionProfiles.profiles.ssh.port%", + "description": "%configuration.SAS.connectionProfiles.profiles.iom.port%", "exclusiveMinimum": 1, "exclusiveMaximum": 65535 } diff --git a/package.nls.json b/package.nls.json index 68098fc98..cb41e7c4a 100644 --- a/package.nls.json +++ b/package.nls.json @@ -39,6 +39,9 @@ "configuration.SAS.connectionProfiles.profiles.connectionType": "SAS Profile Connection Type", "configuration.SAS.connectionProfiles.profiles.context": "SAS Viya Context", "configuration.SAS.connectionProfiles.profiles.endpoint": "SAS Viya Connection Profile Endpoint", + "configuration.SAS.connectionProfiles.profiles.iom.host": "SAS IOM Connection Host", + "configuration.SAS.connectionProfiles.profiles.iom.port": "SAS IOM Connection port", + "configuration.SAS.connectionProfiles.profiles.iom.username": "SAS IOM Connection username", "configuration.SAS.connectionProfiles.profiles.name": "SAS Connection Profile Name", "configuration.SAS.connectionProfiles.profiles.sasOptions": "SAS Connection SAS options", "configuration.SAS.connectionProfiles.profiles.ssh.host": "SAS SSH Connection SSH Host", From 0ac2c03c37d1d74708bfe77c4987bff1cd6c79d4 Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Wed, 22 Nov 2023 07:47:14 -0500 Subject: [PATCH 26/46] move includes -> endswith --- client/src/connection/itc/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/connection/itc/index.ts b/client/src/connection/itc/index.ts index 16e9968ed..e6f796e50 100644 --- a/client/src/connection/itc/index.ts +++ b/client/src/connection/itc/index.ts @@ -308,7 +308,7 @@ do { }; private processLineCodes(line: string): boolean { - if (line.includes(LineCodes.RunEndCode)) { + if (line.endsWith(LineCodes.RunEndCode)) { // run completed this.fetchResults(); return true; From 44cec6a36ec611c6813c5c48a0b879637b0d20c4 Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Wed, 22 Nov 2023 08:20:39 -0500 Subject: [PATCH 27/46] fix filepath --- client/src/connection/itc/index.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/client/src/connection/itc/index.ts b/client/src/connection/itc/index.ts index e6f796e50..272b7f1cb 100644 --- a/client/src/connection/itc/index.ts +++ b/client/src/connection/itc/index.ts @@ -359,11 +359,15 @@ do { globalStorageUri, `${this._html5FileName}.htm`, ); - this._shellProcess.stdin.write( - `$filePath = "${resolve( + const directorySeparator = this._workDirectory.lastIndexOf('/') !== -1 ? '/' : '\\'; + const filePath = this._config.protocol === ITCProtocol.COM + ? resolve( this._workDirectory, this._html5FileName + ".htm", - )}" + ) + : `${this._workDirectory}${directorySeparator}${this._html5FileName}.htm`; + this._shellProcess.stdin.write( + `$filePath = "${filePath}" $outputFile = "${outputFileUri.fsPath}" $runner.FetchResultsFile($filePath, $outputFile)\n`, this.onWriteComplete, From 010a799b083ccab3b2dd9958ad82ff3aaf24686e Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Wed, 22 Nov 2023 08:24:27 -0500 Subject: [PATCH 28/46] run npm run format --- client/src/connection/itc/index.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/client/src/connection/itc/index.ts b/client/src/connection/itc/index.ts index 272b7f1cb..59e245210 100644 --- a/client/src/connection/itc/index.ts +++ b/client/src/connection/itc/index.ts @@ -359,13 +359,12 @@ do { globalStorageUri, `${this._html5FileName}.htm`, ); - const directorySeparator = this._workDirectory.lastIndexOf('/') !== -1 ? '/' : '\\'; - const filePath = this._config.protocol === ITCProtocol.COM - ? resolve( - this._workDirectory, - this._html5FileName + ".htm", - ) - : `${this._workDirectory}${directorySeparator}${this._html5FileName}.htm`; + const directorySeparator = + this._workDirectory.lastIndexOf("/") !== -1 ? "/" : "\\"; + const filePath = + this._config.protocol === ITCProtocol.COM + ? resolve(this._workDirectory, this._html5FileName + ".htm") + : `${this._workDirectory}${directorySeparator}${this._html5FileName}.htm`; this._shellProcess.stdin.write( `$filePath = "${filePath}" $outputFile = "${outputFileUri.fsPath}" From 138ad73095605f3ad0368edca158ff7bf63b615c Mon Sep 17 00:00:00 2001 From: Joe Morris Date: Wed, 13 Dec 2023 14:03:34 -0500 Subject: [PATCH 29/46] fix: set correct encoding on powershell client --- client/src/connection/itc/index.ts | 5 +++-- client/src/connection/itc/script.ts | 7 ++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/client/src/connection/itc/index.ts b/client/src/connection/itc/index.ts index 59e245210..0ebe90e86 100644 --- a/client/src/connection/itc/index.ts +++ b/client/src/connection/itc/index.ts @@ -1,6 +1,6 @@ // Copyright © 2023, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Uri, l10n, window, workspace } from "vscode"; +import { Uri, env, l10n, window, workspace } from "vscode"; import { ChildProcessWithoutNullStreams, spawn } from "child_process"; import { resolve } from "path"; @@ -101,8 +101,9 @@ export class ITCSession extends Session { protocol === ITCProtocol.COM ? "ITC Local" : "ITC IOM Bridge" }"\n`, ); + this._shellProcess.stdin.write(`$displayLang = "${env.language}"\n`); this._shellProcess.stdin.write( - `$runner.Setup($profileHost,$username,$password,$port,$protocol,$serverName)\n`, + `$runner.Setup($profileHost,$username,$password,$port,$protocol,$serverName,$displayLang)\n`, this.onWriteComplete, ); this._shellProcess.stdin.write( diff --git a/client/src/connection/itc/script.ts b/client/src/connection/itc/script.ts index f3ba2a6cb..ac0a8527a 100644 --- a/client/src/connection/itc/script.ts +++ b/client/src/connection/itc/script.ts @@ -18,14 +18,19 @@ class SASRunner{ $varLogs = $this.FlushLog(4096) Write-Host $varLogs } - [void]Setup([string]$profileHost, [string]$username, [string]$password, [int]$port, [int]$protocol, [string]$serverName) { + [void]Setup([string]$profileHost, [string]$username, [string]$password, [int]$port, [int]$protocol, [string]$serverName, [string]$displayLang) { try { + # Set Encoding for input and output + $OutputEncoding = [Console]::InputEncoding = [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding + # create the Integration Technologies objects $objFactory = New-Object -ComObject SASObjectManager.ObjectFactoryMulti2 $objServerDef = New-Object -ComObject SASObjectManager.ServerDef $objServerDef.MachineDNSName = $profileHost # SAS Workspace node $objServerDef.Port = $port # workspace server port $objServerDef.Protocol = $protocol # 0 = COM protocol + $sasLocale = $displayLang -replace '-', '_' + $objServerDef.ExtraNameValuePairs="LOCALE=$sasLocale" # set the correct locale # Class Identifier for SAS Workspace $objServerDef.ClassIdentifier = "440196d4-90f0-11d0-9f41-00a024bb830c" From 0a00464db57c8d327ec11781b1d8587e100f8c63 Mon Sep 17 00:00:00 2001 From: Joe Morris Date: Wed, 13 Dec 2023 17:14:50 -0500 Subject: [PATCH 30/46] chore: fix tests --- client/test/connection/itc/index.test.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/client/test/connection/itc/index.test.ts b/client/test/connection/itc/index.test.ts index 75833cbce..621a6ad21 100644 --- a/client/test/connection/itc/index.test.ts +++ b/client/test/connection/itc/index.test.ts @@ -104,17 +104,18 @@ describe("ITC connection", () => { expect(stdinStub.args[5][0]).to.deep.equal(`$username = ""\n`); expect(stdinStub.args[6][0]).to.deep.equal(`$password = ""\n`); expect(stdinStub.args[7][0]).to.deep.equal(`$serverName = "ITC Local"\n`); - expect(stdinStub.args[8][0]).to.deep.equal( - "$runner.Setup($profileHost,$username,$password,$port,$protocol,$serverName)\n", - ); + expect(stdinStub.args[8][0]).to.deep.equal(`$displayLang = "en"\n`); expect(stdinStub.args[9][0]).to.deep.equal( + "$runner.Setup($profileHost,$username,$password,$port,$protocol,$serverName,$displayLang)\n", + ); + expect(stdinStub.args[10][0]).to.deep.equal( "$runner.ResolveSystemVars()\n", ); - expect(stdinStub.args[10][0]).to.deep.equal( + expect(stdinStub.args[11][0]).to.deep.equal( `$sasOpts=@("-PAGESIZE=MAX")\n`, ); - expect(stdinStub.args[11][0]).to.deep.equal( + expect(stdinStub.args[12][0]).to.deep.equal( `$runner.SetOptions($sasOpts)\n`, ); }); @@ -151,12 +152,12 @@ describe("ITC connection", () => { expect(runResult.html5).to.equal(html5); expect(runResult.title).to.equal("Result"); - expect(stdinStub.args[12][0]).to.deep.equal( + expect(stdinStub.args[13][0]).to.deep.equal( `$code=\n@'\nods html5 path="/work/dir";\nproc print data=sashelp.cars;\nrun;\n%put --vscode-sas-extension-submit-end--;\n'@\n`, ); - expect(stdinStub.args[13][0]).to.deep.equal(`$runner.Run($code)\n`); - expect(stdinStub.args[14][0]).to.contain(`$outputFile = "${tempHtmlPath}" + expect(stdinStub.args[14][0]).to.deep.equal(`$runner.Run($code)\n`); + expect(stdinStub.args[15][0]).to.contain(`$outputFile = "${tempHtmlPath}" $runner.FetchResultsFile($filePath, $outputFile) `); }); From 3830454ba97cd2a9f16463b80c43eceaa03b25aa Mon Sep 17 00:00:00 2001 From: Joe Morris Date: Wed, 13 Dec 2023 18:01:50 -0500 Subject: [PATCH 31/46] chore: run test with en-US locale for deterministic behavior --- client/test/runTest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/test/runTest.ts b/client/test/runTest.ts index 3c2c824b7..889097fc6 100644 --- a/client/test/runTest.ts +++ b/client/test/runTest.ts @@ -16,7 +16,7 @@ async function main() { await runTests({ extensionDevelopmentPath, extensionTestsPath, - launchArgs: ["--disable-extensions"], + launchArgs: ["--disable-extensions", "--locale en-US"], }); } catch (err) { console.error("Failed to run tests"); From 72887db3b1cc3709e02a1a542ae536e66948f65a Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Fri, 15 Dec 2023 14:47:25 -0500 Subject: [PATCH 32/46] fix cancelling --- client/src/commands/run.ts | 2 ++ client/src/connection/itc/index.ts | 34 +++++++++++++++++++----------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/client/src/commands/run.ts b/client/src/commands/run.ts index 8c8dd8cf0..12842b980 100644 --- a/client/src/commands/run.ts +++ b/client/src/commands/run.ts @@ -156,6 +156,8 @@ async function runCode(selected?: boolean, uri?: Uri) { (_progress, cancellationToken) => { cancellationToken.onCancellationRequested(() => { session.cancel?.(); + running = false; + commands.executeCommand("setContext", "SAS.running", false); }); return session.run(code).then((results) => { if (outputHtml && results.html5) { diff --git a/client/src/connection/itc/index.ts b/client/src/connection/itc/index.ts index 0ebe90e86..09f5f6dec 100644 --- a/client/src/connection/itc/index.ts +++ b/client/src/connection/itc/index.ts @@ -1,6 +1,6 @@ // Copyright © 2023, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Uri, env, l10n, window, workspace } from "vscode"; +import { Uri, commands, env, l10n, window, workspace } from "vscode"; import { ChildProcessWithoutNullStreams, spawn } from "child_process"; import { resolve } from "path"; @@ -44,11 +44,13 @@ export class ITCSession extends Session { private _password: string; private _secretStorage; private _passwordKey: string; + private _pollingForLogResults: boolean; constructor() { super(); this._password = ""; this._secretStorage = getSecretStorage(SECRET_STORAGE_NAMESPACE); + this._pollingForLogResults = false; } public set config(value: Config) { @@ -184,6 +186,7 @@ export class ITCSession extends Session { const codeToRun = `$code=\n@'\n${codeWithEnd}\n'@\n`; this._shellProcess.stdin.write(codeToRun); + this._pollingForLogResults = true; this._shellProcess.stdin.write(`$runner.Run($code)\n`, async (error) => { if (error) { this._runReject(error); @@ -223,11 +226,12 @@ export class ITCSession extends Session { * Cancels a running SAS program */ public cancel = async () => { + this._pollingForLogResults = false; this._shellProcess.stdin.write("$runner.Cancel()\n", async (error) => { if (error) { this._runReject(error); } - + await this.fetchLog(); }); }; @@ -248,16 +252,21 @@ export class ITCSession extends Session { * writing each chunk to stdout. */ private fetchLog = async (): Promise => { - this._shellProcess.stdin.write( - ` -do { - $chunkSize = 32768 - $log = $runner.FlushLog($chunkSize) - Write-Host $log -} while ($log.Length -gt 0)\n - `, - this.onWriteComplete, - ); + const pollingInterval = setInterval(() => { + if (!this._pollingForLogResults) { + clearInterval(pollingInterval); + } + this._shellProcess.stdin.write( + ` + do { + $chunkSize = 32768 + $log = $runner.FlushLog($chunkSize) + Write-Host $log + } while ($log.Length -gt 0)\n + `, + this.onWriteComplete, + ); + }, 2 * 1000); }; /** @@ -356,6 +365,7 @@ do { await workspace.fs.createDirectory(globalStorageUri); } + this._pollingForLogResults = false; const outputFileUri = Uri.joinPath( globalStorageUri, `${this._html5FileName}.htm`, From 895d7e81a5db689e319dffa68b02673a06be1ca0 Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Fri, 15 Dec 2023 14:51:29 -0500 Subject: [PATCH 33/46] fix lint/format issues --- client/src/connection/itc/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/connection/itc/index.ts b/client/src/connection/itc/index.ts index 09f5f6dec..31b94b605 100644 --- a/client/src/connection/itc/index.ts +++ b/client/src/connection/itc/index.ts @@ -1,6 +1,6 @@ // Copyright © 2023, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import { Uri, commands, env, l10n, window, workspace } from "vscode"; +import { Uri, env, l10n, window, workspace } from "vscode"; import { ChildProcessWithoutNullStreams, spawn } from "child_process"; import { resolve } from "path"; @@ -231,7 +231,7 @@ export class ITCSession extends Session { if (error) { this._runReject(error); } - + await this.fetchLog(); }); }; From 3dee69224a734f6f78cdaa82f231fab4c89cf253 Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Fri, 22 Dec 2023 10:48:59 -0500 Subject: [PATCH 34/46] Update cancel process --- client/src/commands/run.ts | 4 +--- client/src/connection/itc/index.ts | 5 +++++ client/src/connection/itc/script.ts | 1 + client/src/connection/itc/types.ts | 1 + 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/client/src/commands/run.ts b/client/src/commands/run.ts index 12842b980..08ee2378d 100644 --- a/client/src/commands/run.ts +++ b/client/src/commands/run.ts @@ -154,10 +154,8 @@ async function runCode(selected?: boolean, uri?: Uri) { cancellable: typeof session.cancel === "function", }, (_progress, cancellationToken) => { - cancellationToken.onCancellationRequested(() => { + cancellationToken.onCancellationRequested((...args) => { session.cancel?.(); - running = false; - commands.executeCommand("setContext", "SAS.running", false); }); return session.run(code).then((results) => { if (outputHtml && results.html5) { diff --git a/client/src/connection/itc/index.ts b/client/src/connection/itc/index.ts index 31b94b605..94a4f9930 100644 --- a/client/src/connection/itc/index.ts +++ b/client/src/connection/itc/index.ts @@ -334,6 +334,11 @@ export class ITCSession extends Session { return true; } + if (line.includes(LineCodes.RunCancelledCode)) { + this._runResolve({}); + return true; + } + return false; } diff --git a/client/src/connection/itc/script.ts b/client/src/connection/itc/script.ts index ac0a8527a..610ee0fef 100644 --- a/client/src/connection/itc/script.ts +++ b/client/src/connection/itc/script.ts @@ -102,6 +102,7 @@ class SASRunner{ [void]Cancel(){ try{ $this.objSAS.LanguageService.Cancel() + Write-Host "${LineCodes.RunCancelledCode}" }catch{ throw "Cancel error" } diff --git a/client/src/connection/itc/types.ts b/client/src/connection/itc/types.ts index 914f3d0a6..d784601ed 100644 --- a/client/src/connection/itc/types.ts +++ b/client/src/connection/itc/types.ts @@ -3,6 +3,7 @@ export enum LineCodes { ResultsFetchedCode = "--vscode-sas-extension-results-fetched--", + RunCancelledCode = "--vscode-sas-extension-run-cancelled--", RunEndCode = "--vscode-sas-extension-submit-end--", SessionCreatedCode = "--vscode-sas-extension-session-created--", } From bf8ac6a0a1babd95b1c6683b8fbd7ec17c0b9e06 Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Fri, 22 Dec 2023 10:48:59 -0500 Subject: [PATCH 35/46] Update cancel process --- client/src/commands/run.ts | 2 -- client/src/connection/itc/index.ts | 5 +++++ client/src/connection/itc/script.ts | 1 + client/src/connection/itc/types.ts | 1 + 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/client/src/commands/run.ts b/client/src/commands/run.ts index 12842b980..8c8dd8cf0 100644 --- a/client/src/commands/run.ts +++ b/client/src/commands/run.ts @@ -156,8 +156,6 @@ async function runCode(selected?: boolean, uri?: Uri) { (_progress, cancellationToken) => { cancellationToken.onCancellationRequested(() => { session.cancel?.(); - running = false; - commands.executeCommand("setContext", "SAS.running", false); }); return session.run(code).then((results) => { if (outputHtml && results.html5) { diff --git a/client/src/connection/itc/index.ts b/client/src/connection/itc/index.ts index 31b94b605..94a4f9930 100644 --- a/client/src/connection/itc/index.ts +++ b/client/src/connection/itc/index.ts @@ -334,6 +334,11 @@ export class ITCSession extends Session { return true; } + if (line.includes(LineCodes.RunCancelledCode)) { + this._runResolve({}); + return true; + } + return false; } diff --git a/client/src/connection/itc/script.ts b/client/src/connection/itc/script.ts index ac0a8527a..610ee0fef 100644 --- a/client/src/connection/itc/script.ts +++ b/client/src/connection/itc/script.ts @@ -102,6 +102,7 @@ class SASRunner{ [void]Cancel(){ try{ $this.objSAS.LanguageService.Cancel() + Write-Host "${LineCodes.RunCancelledCode}" }catch{ throw "Cancel error" } diff --git a/client/src/connection/itc/types.ts b/client/src/connection/itc/types.ts index 914f3d0a6..d784601ed 100644 --- a/client/src/connection/itc/types.ts +++ b/client/src/connection/itc/types.ts @@ -3,6 +3,7 @@ export enum LineCodes { ResultsFetchedCode = "--vscode-sas-extension-results-fetched--", + RunCancelledCode = "--vscode-sas-extension-run-cancelled--", RunEndCode = "--vscode-sas-extension-submit-end--", SessionCreatedCode = "--vscode-sas-extension-session-created--", } From 1ffda86cece6cf5b78f7bceb69740be46e578b70 Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Mon, 8 Jan 2024 09:14:28 -0500 Subject: [PATCH 36/46] resolve sas 9 popup error --- client/src/components/profile.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/components/profile.ts b/client/src/components/profile.ts index f7cde8453..9ef9766d4 100644 --- a/client/src/components/profile.ts +++ b/client/src/components/profile.ts @@ -775,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"), From e5eeff5ed8fb271cbec2be51a01b55f8a9e14566 Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Mon, 8 Jan 2024 09:17:54 -0500 Subject: [PATCH 37/46] fix test --- client/test/components/profile/profile.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/test/components/profile/profile.test.ts b/client/test/components/profile/profile.test.ts index 9f8c4baee..3ef72a670 100644 --- a/client/test/components/profile/profile.test.ts +++ b/client/test/components/profile/profile.test.ts @@ -944,8 +944,8 @@ describe("Profiles", async function () { { name: "Host", prompt: ProfilePromptType.Host, - wantTitle: "SAS 9 SSH Server", - wantDescription: "Enter the name of the SAS 9 SSH server.", + wantTitle: "SAS 9 Server", + wantDescription: "Enter the name of the SAS 9 server.", wantPlaceHolder: "Enter the server name", }, { From 1dfdea8a9a84beff34ad295b44da61a468ec7597 Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Mon, 8 Jan 2024 14:57:34 -0500 Subject: [PATCH 38/46] add chcp 65001 to powershell command --- client/src/connection/itc/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/connection/itc/index.ts b/client/src/connection/itc/index.ts index 94a4f9930..c01227660 100644 --- a/client/src/connection/itc/index.ts +++ b/client/src/connection/itc/index.ts @@ -73,7 +73,7 @@ export class ITCSession extends Session { return; // manually terminate to avoid executing the code below } - this._shellProcess = spawn("powershell.exe /nologo -Command -", { + this._shellProcess = spawn("chcp 65001 >NUL & powershell.exe -NonInteractive -NoProfile -Command -", { shell: true, env: process.env, }); From 4c7b373acc85ebb8b741c46fbbe1d5293ba77732 Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Mon, 8 Jan 2024 16:27:31 -0500 Subject: [PATCH 39/46] upgrade prettier, run npm run format, fix tests --- client/src/commands/run.ts | 2 +- client/src/connection/itc/index.ts | 11 +++++++---- client/test/connection/itc/index.test.ts | 7 +++++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/client/src/commands/run.ts b/client/src/commands/run.ts index 08ee2378d..8c8dd8cf0 100644 --- a/client/src/commands/run.ts +++ b/client/src/commands/run.ts @@ -154,7 +154,7 @@ async function runCode(selected?: boolean, uri?: Uri) { cancellable: typeof session.cancel === "function", }, (_progress, cancellationToken) => { - cancellationToken.onCancellationRequested((...args) => { + cancellationToken.onCancellationRequested(() => { session.cancel?.(); }); return session.run(code).then((results) => { diff --git a/client/src/connection/itc/index.ts b/client/src/connection/itc/index.ts index c01227660..c1908ec52 100644 --- a/client/src/connection/itc/index.ts +++ b/client/src/connection/itc/index.ts @@ -73,10 +73,13 @@ export class ITCSession extends Session { return; // manually terminate to avoid executing the code below } - this._shellProcess = spawn("chcp 65001 >NUL & powershell.exe -NonInteractive -NoProfile -Command -", { - shell: true, - env: process.env, - }); + this._shellProcess = spawn( + "chcp 65001 >NUL & powershell.exe -NonInteractive -NoProfile -Command -", + { + shell: true, + env: process.env, + }, + ); this._shellProcess.stdout.on("data", this.onShellStdOut); this._shellProcess.stderr.on("data", this.onShellStdErr); this._shellProcess.stdin.write(scriptContent + "\n", this.onWriteComplete); diff --git a/client/test/connection/itc/index.test.ts b/client/test/connection/itc/index.test.ts index 621a6ad21..95219bd92 100644 --- a/client/test/connection/itc/index.test.ts +++ b/client/test/connection/itc/index.test.ts @@ -87,8 +87,11 @@ describe("ITC connection", () => { await setupPromise; - expect(spawnStub.calledWith("powershell.exe /nologo -Command -")).to.be - .true; + expect( + spawnStub.calledWith( + "chcp 65001 >NUL & powershell.exe -NonInteractive -NoProfile -Command -", + ), + ).to.be.true; //using args here allows use of deep equal, that generates a concise diff in the test output on failures expect(stdinStub.args[0][0]).to.deep.equal(scriptContent + "\n"); From 3b80433ddddc02f46f6b00c2e633547af8fdf477 Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Tue, 9 Jan 2024 09:20:21 -0500 Subject: [PATCH 40/46] fix running w/o ods results --- client/src/connection/itc/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/src/connection/itc/index.ts b/client/src/connection/itc/index.ts index c1908ec52..11e9df4e2 100644 --- a/client/src/connection/itc/index.ts +++ b/client/src/connection/itc/index.ts @@ -188,6 +188,7 @@ export class ITCSession extends Session { const codeWithEnd = `${codeWithODSPath}\n%put ${LineCodes.RunEndCode};`; const codeToRun = `$code=\n@'\n${codeWithEnd}\n'@\n`; + this._html5FileName = ""; this._shellProcess.stdin.write(codeToRun); this._pollingForLogResults = true; this._shellProcess.stdin.write(`$runner.Run($code)\n`, async (error) => { @@ -366,6 +367,10 @@ export class ITCSession extends Session { * Fetches the ODS output results for the latest html results file. */ private fetchResults = async () => { + if (!this._html5FileName) { + return this._runResolve({}); + } + const globalStorageUri = getGlobalStorageUri(); try { await workspace.fs.readDirectory(globalStorageUri); From 5c554aa82eda07693c9997c72e5f95ca591b48ac Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Tue, 9 Jan 2024 09:33:06 -0500 Subject: [PATCH 41/46] add itc connection test script --- doc/scripts/itc-connection-test.ps1 | 177 ++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 doc/scripts/itc-connection-test.ps1 diff --git a/doc/scripts/itc-connection-test.ps1 b/doc/scripts/itc-connection-test.ps1 new file mode 100644 index 000000000..3df7a4e76 --- /dev/null +++ b/doc/scripts/itc-connection-test.ps1 @@ -0,0 +1,177 @@ +<# + Use this script to test itc connections. This roughly mimics the code in `client/src/connection/itc` + and aids in debugging issues with ITC connections. To test, replace any values wrapped in brackets + (i.e. ``) and run with Windows Powershell ISE. +#> +class SASRunner{ + [System.__ComObject] $objSAS + + [void]ResolveSystemVars(){ + $code = +@' + %let workDir = %sysfunc(pathname(work)); + %put &=workDir; + %let rc = %sysfunc(dlgcdir("&workDir")); + run; +'@ + $this.Run($code) + $varLogs = $this.FlushLog(4096) + Write-Host $varLogs + } + [void]Setup([string]$profileHost, [string]$username, [string]$password, [int]$port, [int]$protocol, [string]$serverName, [string]$displayLang) { + try { + # Set Encoding for input and output + $null = cmd /c '' + $Global:OutputEncoding = [Console]::InputEncoding = [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new() + + # create the Integration Technologies objects + $objFactory = New-Object -ComObject SASObjectManager.ObjectFactoryMulti2 + $objServerDef = New-Object -ComObject SASObjectManager.ServerDef + $objServerDef.MachineDNSName = $profileHost # SAS Workspace node + $objServerDef.Port = $port # workspace server port + $objServerDef.Protocol = $protocol # 0 = COM protocol + $sasLocale = $displayLang -replace '-', '_' + $objServerDef.ExtraNameValuePairs="LOCALE=$sasLocale" # set the correct locale + + # Class Identifier for SAS Workspace + $objServerDef.ClassIdentifier = "440196d4-90f0-11d0-9f41-00a024bb830c" + + # create and connect to the SAS session + $this.objSAS = $objFactory.CreateObjectByServer( + $serverName, # server name + $true, + $objServerDef, # built server definition + $username, + $password + ) + + Write-Host "Session created" + } catch { + Write-Error $_ + throw "Setup error" + } + } + + [void]SetOptions([array] $sasOptions) { + $names = [string[]]@() + $values = [string[]]@() + + foreach ($item in $sasOptions) { + $parts = $item -split '=| ' + $names += $parts[0] -replace '^-' + if ($parts.Length -gt 1) { + $values += $parts[1] + } else { + $values += "" + } + } + + [ref]$errorIndices = [int[]]::new($names.Length) + [ref]$errors = [string[]]::new($names.Length) + [ref]$errorCodes = [int[]]::new($names.Length) + + try{ + $this.objSAS.Utilities.OptionService.SetOptions([string[]]$names, [string[]]$values, $errorIndices, $errorCodes, $errors) + + $errVals = $errors.Value + if($errVals.Length -gt 0){ + throw $errVals + } + } catch{ + Write-Error $Error[0].Exception.Message + } + } + + [void]Run([string]$code) { + try{ + $this.objSAS.LanguageService.Reset() + $this.objSAS.LanguageService.Async = $true + $this.objSAS.LanguageService.Submit($code) + }catch{ + Write-Error $_.ScriptStackTrace + throw "Run error" + } + } + + [void]Close(){ + try{ + $this.objSAS.Close() + }catch{ + throw "Close error" + } + } + + [void]Cancel(){ + try{ + $this.objSAS.LanguageService.Cancel() + Write-Host "Run Cancelled" + }catch{ + throw "Cancel error" + } + } + + [String]FlushLog([int]$chunkSize) { + try{ + return $this.objSAS.LanguageService.FlushLog($chunkSize) + } catch{ + throw "FlushLog error" + } + return "" + } + + [void]FlushCompleteLog(){ + do { + $chunkSize = 32768 + $log = $this.FlushLog($chunkSize) + Write-Host $log + } while ($log.Length -gt 0) + } + + [void]FetchResultsFile([string]$filePath, [string]$outputFile) { + $fileRef = "" + $objFile = $this.objSAS.FileService.AssignFileref("outfile", "DISK", $filePath, "", [ref] $fileRef) + $objStream = $objFile.OpenBinaryStream(1); + [Byte[]] $bytes = 0x0 + + $endOfFile = $false + $byteCount = 0 + $outStream = [System.IO.StreamWriter] $outputFile + do + { + $objStream.Read(1024, [ref] $bytes) + $outStream.Write([System.Text.Encoding]::UTF8.GetString($bytes)) + $endOfFile = $bytes.Length -lt 1024 + $byteCount = $byteCount + $bytes.Length + } while (-not $endOfFile) + + $objStream.Close() + $outStream.Close() + $this.objSAS.FileService.DeassignFileref($objFile.FilerefName) + + Write-Host "Results Fetched" + } +} + +$runner = New-Object -TypeName SASRunner +$profileHost = "" +$port = 8591 # Specify IOM port +$protocol = 2 # IOMBridge +$username = "" +$password = "" +$serverName = "ITC IOM Bridge" +$displayLang = "" # EN_US, ZH_CN, etc +$runner.Setup($profileHost,$username,$password,$port,$protocol,$serverName,$displayLang) +$runner.ResolveSystemVars() + +# Uncomment the following line to set any options +# $runner.SetOptions("VALIDVARNAME=ANY") + +# Adjust the value of $code as needed +$code = "proc options group=languagecontrol;" +$runner.Run($code) + +do { + $chunkSize = 32768 + $log = $runner.FlushLog($chunkSize) + Write-Host $log +} while ($log.Length -gt 0) From 04c5829e259bcf66c227977b21fda52016914f8f Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Wed, 10 Jan 2024 10:43:58 -0500 Subject: [PATCH 42/46] fix ods results html --- client/src/components/utils/sasCode.ts | 12 +++++++++++- client/src/connection/itc/index.ts | 9 ++++++--- client/src/connection/ssh/index.ts | 8 +++++--- client/test/connection/itc/index.test.ts | 8 +++++--- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/client/src/components/utils/sasCode.ts b/client/src/components/utils/sasCode.ts index c9599a0be..12effbf78 100644 --- a/client/src/components/utils/sasCode.ts +++ b/client/src/components/utils/sasCode.ts @@ -3,6 +3,7 @@ import { ColorThemeKind, l10n, window, workspace } from "vscode"; import { isAbsolute } from "path"; +import { v4 } from "uuid"; import { getHtmlStyle, isOutputHtmlEnabled } from "./settings"; @@ -48,7 +49,7 @@ 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 { @@ -56,6 +57,15 @@ ${code} } } +export function extractOutputHtmlFileName( + line: string, + defaultValue: string, +): string { + return ( + line.match(/body="(.{8}-.{4}-.{4}-.{4}-.{12}).htm"/)?.[1] ?? defaultValue + ); +} + export async function wrapCodeWithPreambleAndPostamble( code: string, preamble?: string, diff --git a/client/src/connection/itc/index.ts b/client/src/connection/itc/index.ts index 11e9df4e2..00884f7c5 100644 --- a/client/src/connection/itc/index.ts +++ b/client/src/connection/itc/index.ts @@ -11,6 +11,7 @@ import { getSecretStorage, } from "../../components/ExtensionContext"; import { updateStatusBarItem } from "../../components/StatusBarItem"; +import { extractOutputHtmlFileName } from "../../components/utils/sasCode"; import { Session } from "../session"; import { scriptContent } from "./script"; import { LineCodes } from "./types"; @@ -305,6 +306,7 @@ export class ITCSession extends Session { if (!line) { return; } + if (!this._workDirectory && line.startsWith("WORKDIR=")) { const parts = line.split("WORKDIR="); this._workDirectory = parts[1].trim(); @@ -313,9 +315,10 @@ export class ITCSession extends Session { return; } if (!this.processLineCodes(line)) { - this._html5FileName = - line.match(/NOTE: .+ HTML5.* Body.+: (.+)\.htm/)?.[1] ?? - this._html5FileName; + this._html5FileName = extractOutputHtmlFileName( + line, + this._html5FileName, + ); this._onLogFn?.([{ type: "normal", line }]); } }); diff --git a/client/src/connection/ssh/index.ts b/client/src/connection/ssh/index.ts index 6fb157197..91adf4e03 100644 --- a/client/src/connection/ssh/index.ts +++ b/client/src/connection/ssh/index.ts @@ -6,6 +6,7 @@ import { Client, ClientChannel, ConnectConfig } from "ssh2"; import { BaseConfig, RunResult } from ".."; import { updateStatusBarItem } from "../../components/StatusBarItem"; +import { extractOutputHtmlFileName } from "../../components/utils/sasCode"; import { Session } from "../session"; const endCode = "--vscode-sas-extension-submit-end--"; @@ -192,9 +193,10 @@ export class SSHSession extends Session { this.getResult(); } if (!(line.endsWith("?") || line.endsWith(">"))) { - this.html5FileName = - line.match(/NOTE: .+ HTML5.* Body .+: (.+)\.htm/)?.[1] ?? - this.html5FileName; + this.html5FileName = extractOutputHtmlFileName( + line, + this.html5FileName, + ); this._onLogFn?.([{ type: "normal", line }]); } }); diff --git a/client/test/connection/itc/index.test.ts b/client/test/connection/itc/index.test.ts index 95219bd92..4fa0d37dd 100644 --- a/client/test/connection/itc/index.test.ts +++ b/client/test/connection/itc/index.test.ts @@ -6,6 +6,7 @@ import { unlinkSync, writeFileSync } from "fs"; import { join } from "path"; import { SinonSandbox, SinonStub, createSandbox } from "sinon"; import { stubInterface } from "ts-sinon"; +import { v4 } from "uuid"; import { setContext } from "../../../src/components/ExtensionContext"; import { ITCProtocol, getSession } from "../../../src/connection/itc"; @@ -126,7 +127,8 @@ describe("ITC connection", () => { describe("run", () => { const html5 = '
'; - const tempHtmlPath = join(__dirname, "sashtml.htm"); + const htmlLocation = v4(); + const tempHtmlPath = join(__dirname, `${htmlLocation}.htm`); beforeEach(async () => { writeFileSync(tempHtmlPath, html5); const setupPromise = session.setup(); @@ -142,11 +144,11 @@ describe("ITC connection", () => { }); it("calls run function from script", async () => { const runPromise = session.run( - "ods html5;\nproc print data=sashelp.cars;\nrun;", + `ods html5;\nproc print data=sashelp.cars;\nrun;`, ); //simulate log message for body file - onDataCallback(Buffer.from("NOTE: Writing HTML5 Body file: sashtml.htm")); + onDataCallback(Buffer.from(`ods html5 body="${htmlLocation}.htm"`)); //simulate end of submission onDataCallback(Buffer.from(LineCodes.RunEndCode)); onDataCallback(Buffer.from(LineCodes.ResultsFetchedCode)); From ad48cd15c82de5327db9f50411a6e69992178f93 Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Thu, 11 Jan 2024 09:25:14 -0500 Subject: [PATCH 43/46] collect all bytes before string conversion --- client/src/connection/itc/script.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/src/connection/itc/script.ts b/client/src/connection/itc/script.ts index 610ee0fef..4d7fc4997 100644 --- a/client/src/connection/itc/script.ts +++ b/client/src/connection/itc/script.ts @@ -130,6 +130,7 @@ class SASRunner{ $objFile = $this.objSAS.FileService.AssignFileref("outfile", "DISK", $filePath, "", [ref] $fileRef) $objStream = $objFile.OpenBinaryStream(1); [Byte[]] $bytes = 0x0 + [Byte[]] $allBytes = 0x0 $endOfFile = $false $byteCount = 0 @@ -137,11 +138,12 @@ class SASRunner{ do { $objStream.Read(1024, [ref] $bytes) - $outStream.Write([System.Text.Encoding]::UTF8.GetString($bytes)) + $allBytes += $bytes $endOfFile = $bytes.Length -lt 1024 $byteCount = $byteCount + $bytes.Length } while (-not $endOfFile) + $outStream.Write([System.Text.Encoding]::UTF8.GetString($allBytes)) $objStream.Close() $outStream.Close() $this.objSAS.FileService.DeassignFileref($objFile.FilerefName) From a589058ec9f23a9a5ac5d20fa680f8e3e193d209 Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Fri, 12 Jan 2024 08:00:54 -0500 Subject: [PATCH 44/46] Revert "collect all bytes before string conversion" This reverts commit ad48cd15c82de5327db9f50411a6e69992178f93. --- client/src/connection/itc/script.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/client/src/connection/itc/script.ts b/client/src/connection/itc/script.ts index 4d7fc4997..610ee0fef 100644 --- a/client/src/connection/itc/script.ts +++ b/client/src/connection/itc/script.ts @@ -130,7 +130,6 @@ class SASRunner{ $objFile = $this.objSAS.FileService.AssignFileref("outfile", "DISK", $filePath, "", [ref] $fileRef) $objStream = $objFile.OpenBinaryStream(1); [Byte[]] $bytes = 0x0 - [Byte[]] $allBytes = 0x0 $endOfFile = $false $byteCount = 0 @@ -138,12 +137,11 @@ class SASRunner{ do { $objStream.Read(1024, [ref] $bytes) - $allBytes += $bytes + $outStream.Write([System.Text.Encoding]::UTF8.GetString($bytes)) $endOfFile = $bytes.Length -lt 1024 $byteCount = $byteCount + $bytes.Length } while (-not $endOfFile) - $outStream.Write([System.Text.Encoding]::UTF8.GetString($allBytes)) $objStream.Close() $outStream.Close() $this.objSAS.FileService.DeassignFileref($objFile.FilerefName) From 88c6b54623e5bf87b576f95146d5c275a009dd0f Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Fri, 12 Jan 2024 08:34:30 -0500 Subject: [PATCH 45/46] use filestream over streamwriter --- client/src/connection/itc/script.ts | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/client/src/connection/itc/script.ts b/client/src/connection/itc/script.ts index 610ee0fef..be12dfff7 100644 --- a/client/src/connection/itc/script.ts +++ b/client/src/connection/itc/script.ts @@ -133,18 +133,20 @@ class SASRunner{ $endOfFile = $false $byteCount = 0 - $outStream = [System.IO.StreamWriter] $outputFile - do - { - $objStream.Read(1024, [ref] $bytes) - $outStream.Write([System.Text.Encoding]::UTF8.GetString($bytes)) - $endOfFile = $bytes.Length -lt 1024 - $byteCount = $byteCount + $bytes.Length - } while (-not $endOfFile) - - $objStream.Close() - $outStream.Close() - $this.objSAS.FileService.DeassignFileref($objFile.FilerefName) + $outStream = New-Object System.IO.FileStream($outputFile, [System.IO.FileMode]::OpenOrCreate, [System.IO.FileAccess]::Write) + try { + do + { + $objStream.Read(1024, [ref] $bytes) + $outStream.Write($bytes, 0, $bytes.length) + $endOfFile = $bytes.Length -lt 1024 + $byteCount = $byteCount + $bytes.Length + } while (-not $endOfFile) + } finally { + $objStream.Close() + $outStream.Close() + $this.objSAS.FileService.DeassignFileref($objFile.FilerefName) + } Write-Host "${LineCodes.ResultsFetchedCode}" } From 538597b7fdd5aa1e8d52a774b59ae9963208f4f9 Mon Sep 17 00:00:00 2001 From: Scott Dover Date: Fri, 12 Jan 2024 10:53:40 -0500 Subject: [PATCH 46/46] update chunk size --- client/src/connection/itc/script.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/connection/itc/script.ts b/client/src/connection/itc/script.ts index be12dfff7..6b20533de 100644 --- a/client/src/connection/itc/script.ts +++ b/client/src/connection/itc/script.ts @@ -137,9 +137,9 @@ class SASRunner{ try { do { - $objStream.Read(1024, [ref] $bytes) + $objStream.Read(8192, [ref] $bytes) $outStream.Write($bytes, 0, $bytes.length) - $endOfFile = $bytes.Length -lt 1024 + $endOfFile = $bytes.Length -lt 8192 $byteCount = $byteCount + $bytes.Length } while (-not $endOfFile) } finally {