From d48e1517d5397c421d32183c5c2b943a044b5c6b Mon Sep 17 00:00:00 2001 From: Shuguang Sun Date: Fri, 19 Jan 2024 15:32:51 +0800 Subject: [PATCH 01/17] add: saspy Signed-off-by: Shuguang Sun --- client/src/components/profile.ts | 55 +++- client/src/connection/index.ts | 3 + client/src/connection/saspy/index.ts | 416 ++++++++++++++++++++++++++ client/src/connection/saspy/script.ts | 9 + client/src/connection/saspy/types.ts | 9 + 5 files changed, 491 insertions(+), 1 deletion(-) create mode 100644 client/src/connection/saspy/index.ts create mode 100644 client/src/connection/saspy/script.ts create mode 100644 client/src/connection/saspy/types.ts diff --git a/client/src/components/profile.ts b/client/src/components/profile.ts index 9ef9766d4..1e7704adc 100644 --- a/client/src/components/profile.ts +++ b/client/src/components/profile.ts @@ -20,12 +20,14 @@ enum ConnectionOptions { SAS9COM = "SAS 9.4 (local)", SAS9IOM = "SAS 9.4 (remote - IOM)", SAS9SSH = "SAS 9.4 (remote - SSH)", + SAS9SASPY = "SASPY", SASViya = "SAS Viya", } const CONNECTION_PICK_OPTS: string[] = [ ConnectionOptions.SASViya, ConnectionOptions.SAS9SSH, + ConnectionOptions.SAS9SASPY, ConnectionOptions.SAS9IOM, ConnectionOptions.SAS9COM, ]; @@ -60,6 +62,7 @@ export enum ConnectionType { IOM = "iom", Rest = "rest", SSH = "ssh", + SASPY = "saspy", } /** @@ -90,6 +93,13 @@ export interface SSHProfile extends BaseProfile { username: string; } +export interface SASPYProfile extends BaseProfile { + connectionType: ConnectionType.SASPY; + // saspath: string; + cfgname: string; + pythonpath: string; +} + export interface COMProfile extends BaseProfile { connectionType: ConnectionType.COM; host: string; @@ -102,7 +112,7 @@ export interface IOMProfile extends BaseProfile { port: number; } -export type Profile = ViyaProfile | SSHProfile | COMProfile | IOMProfile; +export type Profile = ViyaProfile | SSHProfile | SASPYProfile | COMProfile | IOMProfile; export enum AutoExecType { File = "file", @@ -468,6 +478,15 @@ export class ProfileConfig { pv.error = l10n.t("Missing username in active profile."); return pv; } + } else if (profile.connectionType === ConnectionType.SASPY) { + // if (!profile.cfgname) { + // pv.error = l10n.t("Missing cfgname in active profile."); + // return pv; + // } + // if (!profile.pythonpath) { + // pv.error = l10n.t("Missing Python path in active profile."); + // return pv; + // } } pv.profile = profileDetail.profile; @@ -580,6 +599,26 @@ export class ProfileConfig { return; } + await this.upsertProfile(name, profileClone); + } else if (profileClone.connectionType === ConnectionType.SASPY) { + // profileClone.pythonpath = await createInputTextBox( + // ProfilePromptType.PYTHONPath, + // profileClone.pythonpath, + // ); + // if (profileClone.pythonpath === undefined) { + // return; + // } + + // profileClone.cfgname = await createInputTextBox( + // ProfilePromptType.Cfgname, + // profileClone.cfgname, + // ); + // if (profileClone.cfgname === undefined) { + // return; + // } + + profileClone.cfgname = ""; + // profileClone.sasOptions = []; await this.upsertProfile(name, profileClone); } else if (profileClone.connectionType === ConnectionType.COM) { profileClone.sasOptions = []; @@ -657,6 +696,8 @@ export enum ProfilePromptType { SASPath, Port, Username, + Cfgname, + PYTHONpath } /** @@ -794,6 +835,16 @@ const input: ProfilePromptInput = { placeholder: l10n.t("Enter your username"), description: l10n.t("Enter your SAS server username."), }, + [ProfilePromptType.Cfgname]: { + title: l10n.t("SAS Server Cfgname"), + placeholder: l10n.t("Enter your cfgname"), + description: l10n.t("Enter your SAS server cfgname."), + }, + [ProfilePromptType.PYTHONpath]: { + title: l10n.t("Server Path"), + placeholder: l10n.t("Enter the server path"), + description: l10n.t("Enter the server path of the PYTHON Executable."), + }, }; /** @@ -812,6 +863,8 @@ function mapQuickPickToEnum(connectionTypePickInput: string): ConnectionType { return ConnectionType.Rest; case ConnectionOptions.SAS9SSH: return ConnectionType.SSH; + case ConnectionOptions.SAS9SASPY: + return ConnectionType.SASPY; case ConnectionOptions.SAS9COM: return ConnectionType.COM; case ConnectionOptions.SAS9IOM: diff --git a/client/src/connection/index.ts b/client/src/connection/index.ts index 8a085cab6..b32f6ff3b 100644 --- a/client/src/connection/index.ts +++ b/client/src/connection/index.ts @@ -19,6 +19,7 @@ import { } from "./rest/api/compute"; import { Session } from "./session"; import { getSession as getSSHSession } from "./ssh"; +import { getSession as getSASPYSession } from "./saspy"; let profileConfig: ProfileConfig; @@ -54,6 +55,8 @@ export function getSession(): Session { return getRestSession(toRestConfig(validProfile.profile)); case ConnectionType.SSH: return getSSHSession(validProfile.profile); + case ConnectionType.SASPY: + return getSASPYSession(validProfile.profile); case ConnectionType.COM: return getITCSession(validProfile.profile, ITCProtocol.COM); case ConnectionType.IOM: diff --git a/client/src/connection/saspy/index.ts b/client/src/connection/saspy/index.ts new file mode 100644 index 000000000..162a1682b --- /dev/null +++ b/client/src/connection/saspy/index.ts @@ -0,0 +1,416 @@ +// Copyright © 2023, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { Uri, workspace } from "vscode"; + +import { ChildProcessWithoutNullStreams, spawn } from "child_process"; + +import { BaseConfig, RunResult } from ".."; +import { + getGlobalStorageUri +} 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"; + +let sessionInstance: SASPYSession; + +/** + * Configuration parameters for this connection provider + */ +export interface Config extends BaseConfig { + cfgname: string; + pythonpath: string; +} + +export class SASPYSession extends Session { + private _config: Config; + private _shellProcess: ChildProcessWithoutNullStreams; + private _html5FileName: string; + private _runResolve: ((value?) => void) | undefined; + private _runReject: ((reason?) => void) | undefined; + private _workDirectory: string; + private _pollingForLogResults: boolean; + + public set config(value: Config) { + this._config = value; + } + + /** + * Initialization logic that should be performed prior to execution. + * @returns void promise. + */ + public setup = async (): Promise => { + const setupPromise = new Promise((resolve, reject) => { + this._runResolve = resolve; + this._runReject = reject; + }); + + if (this._shellProcess && !this._shellProcess.killed) { + this._runResolve(); + return; // manually terminate to avoid executing the code below + } + + this._shellProcess = spawn( + `${this._config.pythonpath}`, + ["-i", "-q", "-X utf8"], + // ["-i", "-q"], + { + //shell: true, + // env: process.env, + // env: { + // ...process.env, + // //PATH: process.env.PATH + require('path').delimiter + __dirname, + // PYTHONIOENCODING: "utf-8" + // } + }, + ); + // console.log(`sas \n`); + this._shellProcess.stdout.on("data", this.onShellStdOut); + this._shellProcess.stderr.on("data", this.onShellStdErr); + // console.log(`import saspy\n`); + this._shellProcess.stdin.write(scriptContent + "\n", this.onWriteComplete); + // console.log(`import saspy done\n`); + // this._shellProcess.stdin.write( + // "$runner = New-Object -TypeName SASRunner\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 { cfgname } = this._config; + // console.log(`cfgname\n`); + if (this._config.cfgname?.length > 0) { + // console.log(`sas = saspy.SASsession(cfgname="${cfgname}", results='html')\n`); + this._shellProcess.stdin.write( + ` +sas = saspy.SASsession(cfgname="${cfgname}", results='html') +# sas +\n`, + this.onWriteComplete, + ); + // console.log(`cfgname done\n`); + } else { + // console.log(`cfgname\n`); + // console.log(`sas = saspy.SASsession(results='html')\n`); + this._shellProcess.stdin.write( + ` +sas = saspy.SASsession(results='html') +sas +\n`, + this.onWriteComplete, + ); + // console.log(`cfgname done\n`); + } + + if (this._config.sasOptions?.length > 0) { + // console.log('sas option'); + const sasOptsInput = `$sasOpts=${this.formatSASOptions( + this._config.sasOptions, + )}\n`; + this._shellProcess.stdin.write(sasOptsInput, this.onWriteComplete); + this._shellProcess.stdin.write( + `sas.submit($sasOpts)\n`, + this.onWriteComplete, + ); + } + } + + // free objects in the scripting env + process.on("exit", async () => { + close(); + }); + + return setupPromise; + }; + + /** + * Executes the given input code. + * @param code A string of SAS code to execute. + * @param onLog A callback handler responsible for marshalling log lines back to the higher level extension API. + * @returns A promise that eventually resolves to contain the given {@link RunResult} for the input code execution. + */ + public run = async (code: string): Promise => { + 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 an end mnemonic so that the handler knows when execution has finished + const codeWithEnd = `${codeWithODSPath}\n%put ${LineCodes.RunEndCode};`; + const codeToRun = `code=r""" +${codeWithEnd} +""" +`; + + // console.log("codeToRun = " + code); + + this._html5FileName = ""; + this._shellProcess.stdin.write(codeToRun); + this._pollingForLogResults = true; + this._shellProcess.stdin.write(`ll=sas.submit(code)\n`, async (error) => { + if (error) { + this._runReject(error); + } + + // console.log(`sas to fetchLog`); + await this.fetchLog(); + }); + + return runPromise; + }; + + /** + * Cleans up resources for the given SAS session. + * @returns void promise. + */ + public close = async (): Promise => { + return new Promise((resolve) => { + // console.log(`close`); + + if (this._shellProcess) { + this._shellProcess.stdin.write( + "sas.endsas()\nquit()\n", + this.onWriteComplete, + ); + this._shellProcess.kill(); + this._shellProcess = undefined; + + this._workDirectory = undefined; + this._runReject = undefined; + this._runResolve = undefined; + } + resolve(); + updateStatusBarItem(false); + }); + }; + + /** + * Cancels a running SAS program + */ + public cancel = async () => { + this._pollingForLogResults = false; + this._shellProcess.stdin.write("\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. + * @param sasOptions SAS Options array from the connection profile. + * @returns a string denoting powershell syntax for an array literal. + */ + private formatSASOptions = (sasOptions: string[]): string => { + const optionsVariable = `r"""\n${sasOptions.join(`","`)}\n"""`; + return optionsVariable; + }; + + /** + * Flushes the SAS log in chunks of [chunkSize] length, + * writing each chunk to stdout. + */ + private fetchLog = async (): Promise => { + // console.log('sas to fetching log'); + + // const pollingInterval = setInterval(() => { + // if (!this._pollingForLogResults) { + // clearInterval(pollingInterval); + // } + // this._shellProcess.stdin.write( + // `ll['LOG]\n`, + // this.onWriteComplete, + // ); + // }, 2 * 1000); + this._shellProcess.stdin.write( + `print(ll['LOG'])\n`, + this.onWriteComplete, + ); + // console.log('sas to fetching log done'); + }; + + /** + * Handles stderr output from the powershell child process. + * @param chunk a buffer of stderr output from the child process. + */ + private onShellStdErr = (chunk: Buffer): void => { + const msg = chunk.toString(); + console.warn("shellProcess stderr: " + msg); + this._runReject( + new Error( + "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; + } + }; + + /** + * Handles stdout output from the powershell child process. + * @param data a buffer of stdout output from the child process. + */ + private onShellStdOut = (data: Buffer): void => { + // console.log('sas to fetching out'); + // console.log(data.toString()); + + const output = data.toString().trimEnd(); + const outputLines = output.split(/\n|\r\n/); + + // console.log(output); + + outputLines.forEach((line: string) => { + if (!line) { + return; + } + + if (!this._workDirectory && line.startsWith("WORKDIR=")) { + const parts = line.split("WORKDIR="); + this._workDirectory = parts[1].trim(); + this._runResolve(); + updateStatusBarItem(true); + return; + } + if (!this.processLineCodes(line)) { + this._html5FileName = extractOutputHtmlFileName( + line, + this._html5FileName, + ); + this._onLogFn?.([{ type: "normal", line }]); + } + }); + }; + + private processLineCodes(line: string): boolean { + // console.log('sas linecode'); + if (line.endsWith(LineCodes.RunEndCode)) { + // run completed + // console.log('sas to fetc rest'); + this.fetchResults(); + return true; + } + + if (line.includes(LineCodes.ResultsFetchedCode)) { + this.displayResults(); + return true; + } + + if (line.includes(LineCodes.RunCancelledCode)) { + this._runResolve({}); + 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. + */ + private onWriteComplete = (err: Error): void => { + if (err) { + this._runReject?.(err); + } + }; + + /** + * Not implemented. + */ + public sessionId = (): string => { + throw new Error("Not Implemented"); + }; + + /** + * Fetches the ODS output results for the latest html results file. + */ + private fetchResults = async () => { + // console.log('sas to fetching result'); + if (!this._html5FileName) { + return this._runResolve({}); + } + + const globalStorageUri = getGlobalStorageUri(); + try { + await workspace.fs.readDirectory(globalStorageUri); + } catch (e) { + await workspace.fs.createDirectory(globalStorageUri); + } + + this._pollingForLogResults = false; + const outputFileUri = Uri.joinPath( + 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`; + this._shellProcess.stdin.write( + ` +with open(r"${outputFileUri.fsPath}", 'w') as f1: + f1.write(ll['LST']) +\n`, + this.onWriteComplete, + ); + }; + + 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) { + workspace.fs.delete(outputFileUri); + } + + const runResult: RunResult = {}; + if (htmlResults.search('<*id="IDX*.+">') !== -1) { + runResult.html5 = htmlResults; + runResult.title = "Result"; + } + this._runResolve(runResult); + }; +} + +/** + * Creates a new SAS 9 Session. + * @param c Instance denoting configuration parameters for this connection profile. + * @returns created COM session. + */ +export const getSession = ( + c: Partial, +): Session => { + const defaults = { + cfgname: "", + pythonpath: "python", + }; + + if (!sessionInstance) { + sessionInstance = new SASPYSession(); + } + sessionInstance.config = { ...defaults, ...c }; + return sessionInstance; +}; diff --git a/client/src/connection/saspy/script.ts b/client/src/connection/saspy/script.ts new file mode 100644 index 000000000..7c41d9d95 --- /dev/null +++ b/client/src/connection/saspy/script.ts @@ -0,0 +1,9 @@ +// Copyright © 2023, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// import { LineCodes } from "./types"; + +export const scriptContent = ` +import saspy +import warnings + +`; diff --git a/client/src/connection/saspy/types.ts b/client/src/connection/saspy/types.ts new file mode 100644 index 000000000..d784601ed --- /dev/null +++ b/client/src/connection/saspy/types.ts @@ -0,0 +1,9 @@ +// 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--", + RunCancelledCode = "--vscode-sas-extension-run-cancelled--", + RunEndCode = "--vscode-sas-extension-submit-end--", + SessionCreatedCode = "--vscode-sas-extension-session-created--", +} From 8793aee3d41e9c2ee4f3fdebb6a13ba3e6a2d2b9 Mon Sep 17 00:00:00 2001 From: Shuguang Sun Date: Thu, 21 Mar 2024 16:18:21 +0800 Subject: [PATCH 02/17] chore: saspy Signed-off-by: Shuguang Sun --- client/src/connection/saspy/index.ts | 186 ++++++++++++++------------ client/src/connection/saspy/script.ts | 9 -- 2 files changed, 102 insertions(+), 93 deletions(-) delete mode 100644 client/src/connection/saspy/script.ts diff --git a/client/src/connection/saspy/index.ts b/client/src/connection/saspy/index.ts index 162a1682b..a7b5dad25 100644 --- a/client/src/connection/saspy/index.ts +++ b/client/src/connection/saspy/index.ts @@ -9,9 +9,9 @@ import { getGlobalStorageUri } from "../../components/ExtensionContext"; import { updateStatusBarItem } from "../../components/StatusBarItem"; -import { extractOutputHtmlFileName } from "../../components/utils/sasCode"; +import { extractOutputHtmlFileName, wrapCodeWithOutputHtml } from "../../components/utils/sasCode"; import { Session } from "../session"; -import { scriptContent } from "./script"; +// import { scriptContent } from "./script"; import { LineCodes } from "./types"; let sessionInstance: SASPYSession; @@ -55,23 +55,70 @@ export class SASPYSession extends Session { this._shellProcess = spawn( `${this._config.pythonpath}`, ["-i", "-q", "-X utf8"], - // ["-i", "-q"], { //shell: true, // env: process.env, - // env: { - // ...process.env, - // //PATH: process.env.PATH + require('path').delimiter + __dirname, - // PYTHONIOENCODING: "utf-8" - // } + env: { + ...process.env, + //PATH: process.env.PATH + require('path').delimiter + __dirname, + PYTHONIOENCODING: "utf-8" + } }, ); - // console.log(`sas \n`); this._shellProcess.stdout.on("data", this.onShellStdOut); this._shellProcess.stderr.on("data", this.onShellStdErr); - // console.log(`import saspy\n`); - this._shellProcess.stdin.write(scriptContent + "\n", this.onWriteComplete); - // console.log(`import saspy done\n`); + const saspyWorkDir = ` +%let workDir = %sysfunc(pathname(work)); +%put &=workDir; +%let rc = %sysfunc(dlgcdir("&workDir")); +run; +`; + const saspyWorkDirWithODS = wrapCodeWithOutputHtml(saspyWorkDir); + const saspyHtmlStyle = saspyWorkDirWithODS.match(/style=([^ ]+) /)?.[1] ?? "Illuminate"; + + // const { cfgname } = this._config; + const cfgname = this._config.cfgname?.length > 0 ? this._config.cfgname : ""; + const scriptContent = ` +import saspy + +_cfgname = "${cfgname}" + + +if(not _cfgname): + try: + sas + if sas is None: + sas = saspy.SASsession(cfgname=_cfgname, results='HTML', HTML_Style='${saspyHtmlStyle}') + elif not sas.SASpid: + sas = saspy.SASsession(cfgname=_cfgname, results='HTML', HTML_Style='${saspyHtmlStyle}') + except NameError: + sas = saspy.SASsession(cfgname=_cfgname, results='HTML', HTML_Style='${saspyHtmlStyle}') +else: + try: + sas + if sas is None: + sas = saspy.SASsession(results='HTML', HTML_Style='${saspyHtmlStyle}') + elif not sas.SASpid: + sas = saspy.SASsession(results='HTML', HTML_Style='${saspyHtmlStyle}') + except NameError: + sas = saspy.SASsession(results='HTML', HTML_Style='${saspyHtmlStyle}') + + +try: + sas +except NameError: + raise Exception("Setup error") + + +vscode_saspy_code = r""" +${saspyWorkDir} +""" + +ll=sas.submit(vscode_saspy_code) + +`; + + this._shellProcess.stdin.write(scriptContent + "\n", this.onWriteComplete); // this._shellProcess.stdin.write( // "$runner = New-Object -TypeName SASRunner\n", // this.onWriteComplete, @@ -83,30 +130,8 @@ export class SASPYSession extends Session { * will not exist. The work dir should only be deleted when close is invoked. */ if (!this._workDirectory) { - const { cfgname } = this._config; - // console.log(`cfgname\n`); - if (this._config.cfgname?.length > 0) { - // console.log(`sas = saspy.SASsession(cfgname="${cfgname}", results='html')\n`); - this._shellProcess.stdin.write( - ` -sas = saspy.SASsession(cfgname="${cfgname}", results='html') -# sas -\n`, - this.onWriteComplete, - ); - // console.log(`cfgname done\n`); - } else { - // console.log(`cfgname\n`); - // console.log(`sas = saspy.SASsession(results='html')\n`); - this._shellProcess.stdin.write( - ` -sas = saspy.SASsession(results='html') -sas -\n`, - this.onWriteComplete, - ); - // console.log(`cfgname done\n`); - } + + this._shellProcess.stdin.write(`sas\n`); if (this._config.sasOptions?.length > 0) { // console.log('sas option'); @@ -143,28 +168,31 @@ sas //write ODS output to work so that the session cleans up after itself const codeWithODSPath = code.replace( - "ods html5;", - `ods html5 path="${this._workDirectory}";`, + /\bods html5\(id=vscode\)([^;]*;)/i, + `ods html5(id=vscode) path="${this._workDirectory}"$1`, + ); + const codeWithODSPath2 = codeWithODSPath.replace( + /\bods _all_ close;/i, + ``, ); //write an end mnemonic so that the handler knows when execution has finished - const codeWithEnd = `${codeWithODSPath}\n%put ${LineCodes.RunEndCode};`; - const codeToRun = `code=r""" + const codeWithEnd = `${codeWithODSPath2}\n%put ${LineCodes.RunEndCode};`; + const codeToRun = `codeToRun=r""" ${codeWithEnd} """ `; - // console.log("codeToRun = " + code); + // console.log("codeToRun = " + codeToRun); this._html5FileName = ""; this._shellProcess.stdin.write(codeToRun); this._pollingForLogResults = true; - this._shellProcess.stdin.write(`ll=sas.submit(code)\n`, async (error) => { + await this._shellProcess.stdin.write(`ll=sas.submit(codeToRun, results='HTML')\n`, async (error) => { if (error) { this._runReject(error); } - // console.log(`sas to fetchLog`); await this.fetchLog(); }); @@ -177,8 +205,6 @@ ${codeWithEnd} */ public close = async (): Promise => { return new Promise((resolve) => { - // console.log(`close`); - if (this._shellProcess) { this._shellProcess.stdin.write( "sas.endsas()\nquit()\n", @@ -201,7 +227,7 @@ ${codeWithEnd} */ public cancel = async () => { this._pollingForLogResults = false; - this._shellProcess.stdin.write("\n", async (error) => { + this._shellProcess.stdin.write("print(r'abc')\n", async (error) => { if (error) { this._runReject(error); } @@ -226,22 +252,7 @@ ${codeWithEnd} * writing each chunk to stdout. */ private fetchLog = async (): Promise => { - // console.log('sas to fetching log'); - - // const pollingInterval = setInterval(() => { - // if (!this._pollingForLogResults) { - // clearInterval(pollingInterval); - // } - // this._shellProcess.stdin.write( - // `ll['LOG]\n`, - // this.onWriteComplete, - // ); - // }, 2 * 1000); - this._shellProcess.stdin.write( - `print(ll['LOG'])\n`, - this.onWriteComplete, - ); - // console.log('sas to fetching log done'); + this._shellProcess.stdin.write(`print(ll['LOG'])\n`,this.onWriteComplete,); }; /** @@ -249,15 +260,17 @@ ${codeWithEnd} * @param chunk a buffer of stderr output from the child process. */ private onShellStdErr = (chunk: Buffer): void => { - const msg = chunk.toString(); + const msg = chunk.toString('utf8'); console.warn("shellProcess stderr: " + msg); - this._runReject( - new Error( - "There was an error executing the SAS Program.\nSee console log for more details.", - ), - ); + if (/[^.> ]/.test(msg)) { + this._runReject( + new Error( + "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)) { + if (/^We failed in getConnection|Setup error|spawn .+ ENOENT: Error/i.test(msg)) { this._shellProcess.kill(); this._workDirectory = undefined; } @@ -268,21 +281,23 @@ ${codeWithEnd} * @param data a buffer of stdout output from the child process. */ private onShellStdOut = (data: Buffer): void => { - // console.log('sas to fetching out'); - // console.log(data.toString()); - const output = data.toString().trimEnd(); const outputLines = output.split(/\n|\r\n/); - // console.log(output); - outputLines.forEach((line: string) => { if (!line) { return; } - if (!this._workDirectory && line.startsWith("WORKDIR=")) { - const parts = line.split("WORKDIR="); + // if (!this._workDirectory && line.startsWith("WORKDIR=")) { + // const parts = line.split("WORKDIR="); + // this._workDirectory = parts[1].trim(); + // this._runResolve(); + // updateStatusBarItem(true); + // return; + // } + if (!this._workDirectory && /^WORK Path +=/.test(line)) { + const parts = line.split(/WORK Path +=/); this._workDirectory = parts[1].trim(); this._runResolve(); updateStatusBarItem(true); @@ -299,10 +314,8 @@ ${codeWithEnd} }; private processLineCodes(line: string): boolean { - // console.log('sas linecode'); if (line.endsWith(LineCodes.RunEndCode)) { // run completed - // console.log('sas to fetc rest'); this.fetchResults(); return true; } @@ -341,7 +354,6 @@ ${codeWithEnd} * Fetches the ODS output results for the latest html results file. */ private fetchResults = async () => { - // console.log('sas to fetching result'); if (!this._html5FileName) { return this._runResolve({}); } @@ -364,11 +376,17 @@ ${codeWithEnd} // this._config.protocol === ITCProtocol.COM // ? resolve(this._workDirectory, this._html5FileName + ".htm") // : `${this._workDirectory}${directorySeparator}${this._html5FileName}.htm`; - this._shellProcess.stdin.write( + await this._shellProcess.stdin.write( ` -with open(r"${outputFileUri.fsPath}", 'w') as f1: - f1.write(ll['LST']) -\n`, +with open(r"${outputFileUri.fsPath}", 'w', encoding='utf8') as f1: + f1.write(ll['LST']) + +print(r""" +${LineCodes.ResultsFetchedCode} +""" +) + +`, this.onWriteComplete, ); }; @@ -391,7 +409,7 @@ with open(r"${outputFileUri.fsPath}", 'w') as f1: runResult.html5 = htmlResults; runResult.title = "Result"; } - this._runResolve(runResult); + this._runResolve?.(runResult); }; } diff --git a/client/src/connection/saspy/script.ts b/client/src/connection/saspy/script.ts deleted file mode 100644 index 7c41d9d95..000000000 --- a/client/src/connection/saspy/script.ts +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright © 2023, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -// import { LineCodes } from "./types"; - -export const scriptContent = ` -import saspy -import warnings - -`; From 2b9b4922786891f32ebdfc3cd1b00dc1b3182665 Mon Sep 17 00:00:00 2001 From: Shuguang Sun Date: Mon, 22 Jan 2024 13:18:10 +0800 Subject: [PATCH 03/17] chore: fix html style Signed-off-by: Shuguang Sun --- client/src/connection/saspy/index.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/client/src/connection/saspy/index.ts b/client/src/connection/saspy/index.ts index a7b5dad25..22da8120c 100644 --- a/client/src/connection/saspy/index.ts +++ b/client/src/connection/saspy/index.ts @@ -76,32 +76,30 @@ run; const saspyWorkDirWithODS = wrapCodeWithOutputHtml(saspyWorkDir); const saspyHtmlStyle = saspyWorkDirWithODS.match(/style=([^ ]+) /)?.[1] ?? "Illuminate"; - // const { cfgname } = this._config; const cfgname = this._config.cfgname?.length > 0 ? this._config.cfgname : ""; const scriptContent = ` import saspy _cfgname = "${cfgname}" - if(not _cfgname): try: sas if sas is None: - sas = saspy.SASsession(cfgname=_cfgname, results='HTML', HTML_Style='${saspyHtmlStyle}') + sas = saspy.SASsession(cfgname=_cfgname, results='HTML') elif not sas.SASpid: - sas = saspy.SASsession(cfgname=_cfgname, results='HTML', HTML_Style='${saspyHtmlStyle}') + sas = saspy.SASsession(cfgname=_cfgname, results='HTML') except NameError: - sas = saspy.SASsession(cfgname=_cfgname, results='HTML', HTML_Style='${saspyHtmlStyle}') + sas = saspy.SASsession(cfgname=_cfgname, results='HTML') else: try: sas if sas is None: - sas = saspy.SASsession(results='HTML', HTML_Style='${saspyHtmlStyle}') + sas = saspy.SASsession(results='HTML') elif not sas.SASpid: - sas = saspy.SASsession(results='HTML', HTML_Style='${saspyHtmlStyle}') + sas = saspy.SASsession(results='HTML') except NameError: - sas = saspy.SASsession(results='HTML', HTML_Style='${saspyHtmlStyle}') + sas = saspy.SASsession(results='HTML') try: @@ -109,6 +107,7 @@ try: except NameError: raise Exception("Setup error") +sas.HTML_Style = '${saspyHtmlStyle}' vscode_saspy_code = r""" ${saspyWorkDir} From aae76c4a4bac7b78d904a5020f983dbe3d836119 Mon Sep 17 00:00:00 2001 From: Shuguang Sun Date: Tue, 6 Feb 2024 22:59:15 +0800 Subject: [PATCH 04/17] chore(saspy): fix onlog Signed-off-by: Shuguang Sun --- client/src/connection/saspy/index.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/src/connection/saspy/index.ts b/client/src/connection/saspy/index.ts index 22da8120c..b81489eff 100644 --- a/client/src/connection/saspy/index.ts +++ b/client/src/connection/saspy/index.ts @@ -307,7 +307,12 @@ ${codeWithEnd} line, this._html5FileName, ); - this._onLogFn?.([{ type: "normal", line }]); + + if (this._workDirectory) { + this._onExecutionLogFn?.([{ type: "normal", line }]); + } else { + this._onSessionLogFn?.([{ type: "normal", line }]); + } } }); }; From 0a8151c8884c1b8f0ba4d89055ef4a2dd635ae42 Mon Sep 17 00:00:00 2001 From: Shuguang Sun Date: Thu, 21 Mar 2024 16:58:21 +0800 Subject: [PATCH 05/17] fix: setup to establishConnection Signed-off-by: Shuguang Sun --- client/src/connection/saspy/index.ts | 38 +++++++++++++++++++++++----- client/src/connection/saspy/types.ts | 1 + 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/client/src/connection/saspy/index.ts b/client/src/connection/saspy/index.ts index b81489eff..77049221b 100644 --- a/client/src/connection/saspy/index.ts +++ b/client/src/connection/saspy/index.ts @@ -4,7 +4,7 @@ import { Uri, workspace } from "vscode"; import { ChildProcessWithoutNullStreams, spawn } from "child_process"; -import { BaseConfig, RunResult } from ".."; +import { BaseConfig, LogLineTypeEnum, RunResult } from ".."; import { getGlobalStorageUri } from "../../components/ExtensionContext"; @@ -14,6 +14,19 @@ import { Session } from "../session"; // import { scriptContent } from "./script"; import { LineCodes } from "./types"; +const LogLineTypes: LogLineTypeEnum[] = [ + "normal", + "hilighted", + "source", + "title", + "byline", + "footnote", + "error", + "warning", + "note", + "message", +]; + let sessionInstance: SASPYSession; /** @@ -32,7 +45,8 @@ export class SASPYSession extends Session { private _runReject: ((reason?) => void) | undefined; private _workDirectory: string; private _pollingForLogResults: boolean; - + private _logLineType = 0; + public set config(value: Config) { this._config = value; } @@ -41,7 +55,7 @@ export class SASPYSession extends Session { * Initialization logic that should be performed prior to execution. * @returns void promise. */ - public setup = async (): Promise => { + protected establishConnection = async (): Promise => { const setupPromise = new Promise((resolve, reject) => { this._runResolve = resolve; this._runReject = reject; @@ -133,7 +147,6 @@ ll=sas.submit(vscode_saspy_code) this._shellProcess.stdin.write(`sas\n`); if (this._config.sasOptions?.length > 0) { - // console.log('sas option'); const sasOptsInput = `$sasOpts=${this.formatSASOptions( this._config.sasOptions, )}\n`; @@ -309,9 +322,9 @@ ${codeWithEnd} ); if (this._workDirectory) { - this._onExecutionLogFn?.([{ type: "normal", line }]); + this._onExecutionLogFn?.([{ type: this.getLogLineType(), line }]); } else { - this._onSessionLogFn?.([{ type: "normal", line }]); + this._onSessionLogFn?.([{ type: this.getLogLineType(), line }]); } } }); @@ -334,9 +347,22 @@ ${codeWithEnd} return true; } + if (line.includes(LineCodes.LogLineType)) { + const start = + line.indexOf(LineCodes.LogLineType) + LineCodes.LogLineType.length + 1; + this._logLineType = parseInt(line.slice(start, start + 1)); + return true; + } + return false; } + private getLogLineType(): LogLineTypeEnum { + const result = LogLineTypes[this._logLineType]; + this._logLineType = 0; + return result; + } + /** * Generic call for use on stdin write completion. * @param err The error encountered on the write attempt. Undefined if no error occurred. diff --git a/client/src/connection/saspy/types.ts b/client/src/connection/saspy/types.ts index d784601ed..0e4734bfc 100644 --- a/client/src/connection/saspy/types.ts +++ b/client/src/connection/saspy/types.ts @@ -6,4 +6,5 @@ export enum LineCodes { RunCancelledCode = "--vscode-sas-extension-run-cancelled--", RunEndCode = "--vscode-sas-extension-submit-end--", SessionCreatedCode = "--vscode-sas-extension-session-created--", + LogLineType = "--vscode-sas-extension-log-line-type--", } From bedb3f9eaa7554d4aa40017e27d651369e8ba57d Mon Sep 17 00:00:00 2001 From: Shuguang Sun Date: Tue, 18 Jun 2024 17:39:51 +0800 Subject: [PATCH 06/17] feat: add library support for SASPy profile Signed-off-by: Shuguang Sun --- client/src/commands/authorize.ts | 4 + .../LibraryNavigator/LibraryAdapterFactory.ts | 3 + client/src/components/profile.ts | 30 +- client/src/connection/index.ts | 2 +- client/src/connection/saspy/CodeRunner.ts | 72 +++++ .../connection/saspy/SaspyLibraryAdapter.ts | 298 ++++++++++++++++++ client/src/connection/saspy/index.ts | 83 +++-- client/src/connection/saspy/types.ts | 9 + 8 files changed, 436 insertions(+), 65 deletions(-) create mode 100644 client/src/connection/saspy/CodeRunner.ts create mode 100644 client/src/connection/saspy/SaspyLibraryAdapter.ts diff --git a/client/src/commands/authorize.ts b/client/src/commands/authorize.ts index 9a1b1d090..b8debbdab 100644 --- a/client/src/commands/authorize.ts +++ b/client/src/commands/authorize.ts @@ -43,6 +43,10 @@ export const checkProfileAndAuthorize = commands.executeCommand("setContext", "SAS.librariesDisplayed", true); libraryNavigator.refresh(); return finishAuthorization(profileConfig); + case ConnectionType.SASPY: + commands.executeCommand("setContext", "SAS.librariesDisplayed", true); + libraryNavigator.refresh(); + return finishAuthorization(profileConfig); default: return finishAuthorization(profileConfig); } diff --git a/client/src/components/LibraryNavigator/LibraryAdapterFactory.ts b/client/src/components/LibraryNavigator/LibraryAdapterFactory.ts index c4e354339..fedb5e753 100644 --- a/client/src/components/LibraryNavigator/LibraryAdapterFactory.ts +++ b/client/src/components/LibraryNavigator/LibraryAdapterFactory.ts @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import ItcLibraryAdapter from "../../connection/itc/ItcLibraryAdapter"; import RestLibraryAdapter from "../../connection/rest/RestLibraryAdapter"; +import SaspyLibraryAdapter from "../../connection/saspy/SaspyLibraryAdapter"; import { ConnectionType } from "../profile"; import { LibraryAdapter } from "./types"; @@ -11,6 +12,8 @@ class LibraryAdapterFactory { case ConnectionType.IOM: case ConnectionType.COM: return new ItcLibraryAdapter(); + case ConnectionType.SASPY: + return new SaspyLibraryAdapter(); case ConnectionType.Rest: default: return new RestLibraryAdapter(); diff --git a/client/src/components/profile.ts b/client/src/components/profile.ts index 1e7704adc..4c10cdf3f 100644 --- a/client/src/components/profile.ts +++ b/client/src/components/profile.ts @@ -20,7 +20,7 @@ enum ConnectionOptions { SAS9COM = "SAS 9.4 (local)", SAS9IOM = "SAS 9.4 (remote - IOM)", SAS9SSH = "SAS 9.4 (remote - SSH)", - SAS9SASPY = "SASPY", + SAS9SASPY = "SAS 9.4 (SASPy)", SASViya = "SAS Viya", } @@ -95,7 +95,6 @@ export interface SSHProfile extends BaseProfile { export interface SASPYProfile extends BaseProfile { connectionType: ConnectionType.SASPY; - // saspath: string; cfgname: string; pythonpath: string; } @@ -112,7 +111,12 @@ export interface IOMProfile extends BaseProfile { port: number; } -export type Profile = ViyaProfile | SSHProfile | SASPYProfile | COMProfile | IOMProfile; +export type Profile = + | ViyaProfile + | SSHProfile + | SASPYProfile + | COMProfile + | IOMProfile; export enum AutoExecType { File = "file", @@ -601,24 +605,8 @@ export class ProfileConfig { await this.upsertProfile(name, profileClone); } else if (profileClone.connectionType === ConnectionType.SASPY) { - // profileClone.pythonpath = await createInputTextBox( - // ProfilePromptType.PYTHONPath, - // profileClone.pythonpath, - // ); - // if (profileClone.pythonpath === undefined) { - // return; - // } - - // profileClone.cfgname = await createInputTextBox( - // ProfilePromptType.Cfgname, - // profileClone.cfgname, - // ); - // if (profileClone.cfgname === undefined) { - // return; - // } - profileClone.cfgname = ""; - // profileClone.sasOptions = []; + // profileClone.sasOptions = []; await this.upsertProfile(name, profileClone); } else if (profileClone.connectionType === ConnectionType.COM) { profileClone.sasOptions = []; @@ -697,7 +685,7 @@ export enum ProfilePromptType { Port, Username, Cfgname, - PYTHONpath + PYTHONpath, } /** diff --git a/client/src/connection/index.ts b/client/src/connection/index.ts index b32f6ff3b..d56053403 100644 --- a/client/src/connection/index.ts +++ b/client/src/connection/index.ts @@ -17,9 +17,9 @@ import { LogLine as ComputeLogLine, LogLineTypeEnum as ComputeLogLineTypeEnum, } from "./rest/api/compute"; +import { getSession as getSASPYSession } from "./saspy"; import { Session } from "./session"; import { getSession as getSSHSession } from "./ssh"; -import { getSession as getSASPYSession } from "./saspy"; let profileConfig: ProfileConfig; diff --git a/client/src/connection/saspy/CodeRunner.ts b/client/src/connection/saspy/CodeRunner.ts new file mode 100644 index 000000000..8cbc41e10 --- /dev/null +++ b/client/src/connection/saspy/CodeRunner.ts @@ -0,0 +1,72 @@ +// Copyright © 2024, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { commands } from "vscode"; + +import { SASPYSession } from "."; +import { LogLine, getSession } from ".."; +import { useRunStore } from "../../store"; + +class CodeRunner { + public async runCode( + code: string, + startTag: string = "", + endTag: string = "", + ): Promise { + // If we're already executing code, lets wait for it + // to finish up. + let unsubscribe; + if (useRunStore.getState().isExecutingCode) { + await new Promise((resolve) => { + unsubscribe = useRunStore.subscribe( + (state) => state.isExecutingCode, + (isExecutingCode) => !isExecutingCode && resolve(true), + ); + }); + } + + const { setIsExecutingCode } = useRunStore.getState(); + setIsExecutingCode(true); + commands.executeCommand("setContext", "SAS.running", true); + const session = getSession(); + + let logText = ""; + const onExecutionLogFn = session.onExecutionLogFn; + const outputLines = []; + const addLine = (logLines: LogLine[]) => + outputLines.push(...logLines.map(({ line }) => line)); + + try { + await session.setup(true); + + // Lets capture output to use it on + session.onExecutionLogFn = addLine; + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + await (session as SASPYSession).run(code); + + const logOutput = outputLines.filter((line) => line.trim()).join(""); + + logText = + startTag && endTag + ? logOutput + .slice( + logOutput.lastIndexOf(startTag), + logOutput.lastIndexOf(endTag), + ) + .replace(startTag, "") + .replace(endTag, "") + : logOutput; + } finally { + unsubscribe && unsubscribe(); + // Lets update our session to write to the log + session.onExecutionLogFn = onExecutionLogFn; + + setIsExecutingCode(false); + commands.executeCommand("setContext", "SAS.running", false); + } + + return logText; + } +} + +export default CodeRunner; diff --git a/client/src/connection/saspy/SaspyLibraryAdapter.ts b/client/src/connection/saspy/SaspyLibraryAdapter.ts new file mode 100644 index 000000000..3eeb88f1b --- /dev/null +++ b/client/src/connection/saspy/SaspyLibraryAdapter.ts @@ -0,0 +1,298 @@ +// Copyright © 2024, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { commands, l10n } from "vscode"; + +import { ChildProcessWithoutNullStreams } from "child_process"; + +import { onRunError } from "../../commands/run"; +import { + LibraryAdapter, + LibraryItem, + TableData, + TableRow, +} from "../../components/LibraryNavigator/types"; +import { Column, ColumnCollection } from "../rest/api/compute"; +import CodeRunner from "./CodeRunner"; +import { Config } from "./types"; + +class SaspyLibraryAdapter implements LibraryAdapter { + protected hasEstablishedConnection: boolean = false; + protected shellProcess: ChildProcessWithoutNullStreams; + protected pollingForLogResults: boolean = false; + protected log: string[] = []; + protected endTag: string = ""; + protected outputFinished: boolean = false; + protected config: Config; + protected codeRunner = new CodeRunner(); + + public async connect(): Promise { + this.hasEstablishedConnection = true; + } + + public async setup(): Promise { + if (this.hasEstablishedConnection) { + return; + } + + await this.connect(); + } + + public async deleteTable(item: LibraryItem): Promise { + const code = ` + proc datasets library=${item.library} nolist nodetails; delete ${item.name}; run; + `; + + await this.runCode(code); + } + + public async getColumns(item: LibraryItem): Promise { + const sql = ` + %let OUTPUT; + proc sql; + select catx(',', name, type, varnum) as column into: OUTPUT separated by '~' + from sashelp.vcolumn + where libname='${item.library}' and memname='${item.name}' + order by varnum; + quit; + %put ; %put &OUTPUT; %put ; + `; + + const columnLines = processQueryRows( + await this.runCode(sql, "", ""), + ); + + const columns = columnLines.map((lineText): Column => { + const [name, type, index] = lineText.split(","); + + return { + name, + type, + index: parseInt(index, 10), + }; + }); + + return { + items: columns, + count: -1, + }; + } + + public async getLibraries(): Promise<{ + items: LibraryItem[]; + count: number; + }> { + const sql = ` + %let OUTPUT; + proc sql; + select catx(',', libname, readonly) as libname_target into: OUTPUT separated by '~' + from sashelp.vlibnam order by libname asc; + quit; + %put ; %put &OUTPUT; %put ; + `; + + const libNames = processQueryRows( + await this.runCode(sql, "", ""), + ); + + const libraries = libNames.map((lineText): LibraryItem => { + const [libName, readOnlyValue] = lineText.split(","); + + return { + type: "library", + uid: libName, + id: libName, + name: libName, + readOnly: readOnlyValue === "yes", + }; + }); + + return { + items: libraries, + count: -1, + }; + } + + public async getRows( + item: LibraryItem, + start: number, + limit: number, + ): Promise { + const { rows: rawRowValues, count } = await this.getDatasetInformation( + item, + start, + limit, + ); + + const rows = rawRowValues.map((line, idx: number): TableRow => { + const rowData = [`${start + idx + 1}`].concat(line); + return { cells: rowData }; + }); + + return { + rows, + count, + }; + } + + public async getRowsAsCSV( + item: LibraryItem, + start: number, + limit: number, + ): Promise { + // We only need the columns for the first page of results + const columns = + start === 0 + ? { + columns: ["INDEX"].concat( + (await this.getColumns(item)).items.map((column) => column.name), + ), + } + : {}; + + const { rows } = await this.getRows(item, start, limit); + + rows.unshift(columns); + + // Fetching csv doesn't rely on count. Instead, we get the count + // upfront via getTableRowCount + return { rows, count: -1 }; + } + + public async getTableRowCount( + item: LibraryItem, + ): Promise<{ rowCount: number; maxNumberOfRowsToRead: number }> { + const code = ` + proc sql; + SELECT COUNT(1) into: COUNT FROM ${item.library}.${item.name}; + quit; + %put &COUNT; + `; + + const output = await this.runCode(code, "", ""); + const rowCount = parseInt(output.replace(/[^0-9]/g, ""), 10); + + return { rowCount, maxNumberOfRowsToRead: 100 }; + } + + public async getTables(item: LibraryItem): Promise<{ + items: LibraryItem[]; + count: number; + }> { + const sql = ` + %let OUTPUT; + proc sql; + select memname into: OUTPUT separated by '~' + from sashelp.vtable + where libname='${item.name!}' + order by memname asc; + quit; + %put ; %put &OUTPUT; %put ; + `; + + const tableNames = processQueryRows( + await this.runCode(sql, "", ""), + ); + + const tables = tableNames.map((lineText): LibraryItem => { + const [table] = lineText.split(","); + + return { + type: "table", + uid: `${item.name!}.${table}`, + id: table, + name: table, + library: item.name, + readOnly: item.readOnly, + }; + }); + + return { items: tables, count: -1 }; + } + + protected async getDatasetInformation( + item: LibraryItem, + start: number, + limit: number, + ): Promise<{ rows: Array; count: number }> { + const maxTableNameLength = 32; + const tempTable = `${item.name}${hms()}${start}`.substring( + 0, + maxTableNameLength, + ); + const code = ` + options nonotes nosource nodate nonumber; + %let COUNT; + proc sql; + SELECT COUNT(1) into: COUNT FROM ${item.library}.${item.name}; + quit; + data work.${tempTable}; + set ${item.library}.${item.name}; + if ${start + 1} <= _N_ <= ${start + limit} then output; + run; + + filename out temp; + proc json nokeys out=out pretty; export work.${tempTable}; run; + + %put ; + %put &COUNT; + data _null_; infile out; input; put _infile_; run; + %put ; + proc datasets library=work nolist nodetails; delete ${tempTable}; run; + options notes source date number; + `; + + let output = await this.runCode(code, "", ""); + + // Extract result count + const countRegex = /(.*)<\/Count>/; + const countMatches = output.match(countRegex); + const count = parseInt(countMatches[1].replace(/\s|\n/gm, ""), 10); + output = output.replace(countRegex, ""); + + const rows = output.replace(/\n|\t/gm, ""); + try { + const tableData = JSON.parse(rows); + return { rows: tableData[`SASTableData+${tempTable}`], count }; + } catch (e) { + console.warn("Failed to load table data with error", e); + console.warn("Raw output", rows); + throw new Error( + l10n.t( + "An error was encountered when loading table data. This usually happens when a table is too large or the data couldn't be processed. See console for more details.", + ), + ); + } + } + + protected async runCode( + code: string, + startTag: string = "", + endTag: string = "", + ): Promise { + try { + return await this.codeRunner.runCode(code, startTag, endTag); + } catch (e) { + onRunError(e); + commands.executeCommand("setContext", "SAS.librariesDisplayed", false); + return ""; + } + } +} + +const processQueryRows = (response: string): string[] => { + const processedResponse = response.trim().replace(/\n|\t/gm, ""); + if (!processedResponse) { + return []; + } + + return processedResponse + .split("~") + .filter((value, index, array) => array.indexOf(value) === index); +}; + +const hms = () => { + const date = new Date(); + return `${date.getHours()}${date.getMinutes()}${date.getSeconds()}`; +}; + +export default SaspyLibraryAdapter; diff --git a/client/src/connection/saspy/index.ts b/client/src/connection/saspy/index.ts index 77049221b..70376d486 100644 --- a/client/src/connection/saspy/index.ts +++ b/client/src/connection/saspy/index.ts @@ -4,15 +4,16 @@ import { Uri, workspace } from "vscode"; import { ChildProcessWithoutNullStreams, spawn } from "child_process"; -import { BaseConfig, LogLineTypeEnum, RunResult } from ".."; -import { - getGlobalStorageUri -} from "../../components/ExtensionContext"; +import { LogLineTypeEnum, RunResult } from ".."; +import { getGlobalStorageUri } from "../../components/ExtensionContext"; import { updateStatusBarItem } from "../../components/StatusBarItem"; -import { extractOutputHtmlFileName, wrapCodeWithOutputHtml } from "../../components/utils/sasCode"; +import { + extractOutputHtmlFileName, + wrapCodeWithOutputHtml, +} from "../../components/utils/sasCode"; import { Session } from "../session"; // import { scriptContent } from "./script"; -import { LineCodes } from "./types"; +import { Config, LineCodes } from "./types"; const LogLineTypes: LogLineTypeEnum[] = [ "normal", @@ -29,14 +30,6 @@ const LogLineTypes: LogLineTypeEnum[] = [ let sessionInstance: SASPYSession; -/** - * Configuration parameters for this connection provider - */ -export interface Config extends BaseConfig { - cfgname: string; - pythonpath: string; -} - export class SASPYSession extends Session { private _config: Config; private _shellProcess: ChildProcessWithoutNullStreams; @@ -46,7 +39,7 @@ export class SASPYSession extends Session { private _workDirectory: string; private _pollingForLogResults: boolean; private _logLineType = 0; - + public set config(value: Config) { this._config = value; } @@ -73,10 +66,10 @@ export class SASPYSession extends Session { //shell: true, // env: process.env, env: { - ...process.env, - //PATH: process.env.PATH + require('path').delimiter + __dirname, - PYTHONIOENCODING: "utf-8" - } + ...process.env, + //PATH: process.env.PATH + require('path').delimiter + __dirname, + PYTHONIOENCODING: "utf-8", + }, }, ); this._shellProcess.stdout.on("data", this.onShellStdOut); @@ -87,11 +80,13 @@ export class SASPYSession extends Session { %let rc = %sysfunc(dlgcdir("&workDir")); run; `; - const saspyWorkDirWithODS = wrapCodeWithOutputHtml(saspyWorkDir); - const saspyHtmlStyle = saspyWorkDirWithODS.match(/style=([^ ]+) /)?.[1] ?? "Illuminate"; + const saspyWorkDirWithODS = wrapCodeWithOutputHtml(saspyWorkDir); + const saspyHtmlStyle = + saspyWorkDirWithODS.match(/style=([^ ]+) /)?.[1] ?? "Illuminate"; - const cfgname = this._config.cfgname?.length > 0 ? this._config.cfgname : ""; - const scriptContent = ` + const cfgname = + this._config.cfgname?.length > 0 ? this._config.cfgname : ""; + const scriptContent = ` import saspy _cfgname = "${cfgname}" @@ -131,7 +126,7 @@ ll=sas.submit(vscode_saspy_code) `; - this._shellProcess.stdin.write(scriptContent + "\n", this.onWriteComplete); + this._shellProcess.stdin.write(scriptContent + "\n", this.onWriteComplete); // this._shellProcess.stdin.write( // "$runner = New-Object -TypeName SASRunner\n", // this.onWriteComplete, @@ -143,8 +138,7 @@ ll=sas.submit(vscode_saspy_code) * will not exist. The work dir should only be deleted when close is invoked. */ if (!this._workDirectory) { - - this._shellProcess.stdin.write(`sas\n`); + this._shellProcess.stdin.write(`sas\n`); if (this._config.sasOptions?.length > 0) { const sasOptsInput = `$sasOpts=${this.formatSASOptions( @@ -183,10 +177,7 @@ ll=sas.submit(vscode_saspy_code) /\bods html5\(id=vscode\)([^;]*;)/i, `ods html5(id=vscode) path="${this._workDirectory}"$1`, ); - const codeWithODSPath2 = codeWithODSPath.replace( - /\bods _all_ close;/i, - ``, - ); + const codeWithODSPath2 = codeWithODSPath.replace(/\bods _all_ close;/i, ``); //write an end mnemonic so that the handler knows when execution has finished const codeWithEnd = `${codeWithODSPath2}\n%put ${LineCodes.RunEndCode};`; @@ -200,13 +191,16 @@ ${codeWithEnd} this._html5FileName = ""; this._shellProcess.stdin.write(codeToRun); this._pollingForLogResults = true; - await this._shellProcess.stdin.write(`ll=sas.submit(codeToRun, results='HTML')\n`, async (error) => { - if (error) { - this._runReject(error); - } + await this._shellProcess.stdin.write( + `ll=sas.submit(codeToRun, results='HTML')\n`, + async (error) => { + if (error) { + this._runReject(error); + } - await this.fetchLog(); - }); + await this.fetchLog(); + }, + ); return runPromise; }; @@ -239,7 +233,7 @@ ${codeWithEnd} */ public cancel = async () => { this._pollingForLogResults = false; - this._shellProcess.stdin.write("print(r'abc')\n", async (error) => { + this._shellProcess.stdin.write("print(r'abc')\n", async (error) => { if (error) { this._runReject(error); } @@ -264,7 +258,7 @@ ${codeWithEnd} * writing each chunk to stdout. */ private fetchLog = async (): Promise => { - this._shellProcess.stdin.write(`print(ll['LOG'])\n`,this.onWriteComplete,); + this._shellProcess.stdin.write(`print(ll['LOG'])\n`, this.onWriteComplete); }; /** @@ -272,7 +266,7 @@ ${codeWithEnd} * @param chunk a buffer of stderr output from the child process. */ private onShellStdErr = (chunk: Buffer): void => { - const msg = chunk.toString('utf8'); + const msg = chunk.toString("utf8"); console.warn("shellProcess stderr: " + msg); if (/[^.> ]/.test(msg)) { this._runReject( @@ -282,7 +276,11 @@ ${codeWithEnd} ); } // If we encountered an error in setup, we need to go through everything again - if (/^We failed in getConnection|Setup error|spawn .+ ENOENT: Error/i.test(msg)) { + if ( + /^We failed in getConnection|Setup error|spawn .+ ENOENT: Error/i.test( + msg, + ) + ) { this._shellProcess.kill(); this._workDirectory = undefined; } @@ -385,6 +383,7 @@ ${codeWithEnd} */ private fetchResults = async () => { if (!this._html5FileName) { + this._pollingForLogResults = false; return this._runResolve({}); } @@ -448,9 +447,7 @@ ${LineCodes.ResultsFetchedCode} * @param c Instance denoting configuration parameters for this connection profile. * @returns created COM session. */ -export const getSession = ( - c: Partial, -): Session => { +export const getSession = (c: Partial): Session => { const defaults = { cfgname: "", pythonpath: "python", diff --git a/client/src/connection/saspy/types.ts b/client/src/connection/saspy/types.ts index 0e4734bfc..5e044a652 100644 --- a/client/src/connection/saspy/types.ts +++ b/client/src/connection/saspy/types.ts @@ -1,5 +1,6 @@ // Copyright © 2023, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { BaseConfig } from ".."; export enum LineCodes { ResultsFetchedCode = "--vscode-sas-extension-results-fetched--", @@ -8,3 +9,11 @@ export enum LineCodes { SessionCreatedCode = "--vscode-sas-extension-session-created--", LogLineType = "--vscode-sas-extension-log-line-type--", } + +/** + * Configuration parameters for this connection provider + */ +export interface Config extends BaseConfig { + cfgname: string; + pythonpath: string; +} From 4f55d0cbc84532b84fa25e827bdcebdd7951a974 Mon Sep 17 00:00:00 2001 From: Shuguang Sun Date: Thu, 23 May 2024 14:54:53 +0800 Subject: [PATCH 07/17] refactor(core): according to the change of main branch Signed-off-by: Shuguang Sun --- client/src/connection/saspy/index.ts | 12 ++++------ client/src/connection/saspy/util.ts | 33 ++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 8 deletions(-) create mode 100644 client/src/connection/saspy/util.ts diff --git a/client/src/connection/saspy/index.ts b/client/src/connection/saspy/index.ts index 70376d486..8bf9c197b 100644 --- a/client/src/connection/saspy/index.ts +++ b/client/src/connection/saspy/index.ts @@ -7,13 +7,11 @@ import { ChildProcessWithoutNullStreams, spawn } from "child_process"; import { LogLineTypeEnum, RunResult } from ".."; import { getGlobalStorageUri } from "../../components/ExtensionContext"; import { updateStatusBarItem } from "../../components/StatusBarItem"; -import { - extractOutputHtmlFileName, - wrapCodeWithOutputHtml, -} from "../../components/utils/sasCode"; import { Session } from "../session"; +import { extractOutputHtmlFileName } from "../util"; // import { scriptContent } from "./script"; import { Config, LineCodes } from "./types"; +import { saspyGetHtmlStyleValue } from "./util"; const LogLineTypes: LogLineTypeEnum[] = [ "normal", @@ -80,9 +78,7 @@ export class SASPYSession extends Session { %let rc = %sysfunc(dlgcdir("&workDir")); run; `; - const saspyWorkDirWithODS = wrapCodeWithOutputHtml(saspyWorkDir); - const saspyHtmlStyle = - saspyWorkDirWithODS.match(/style=([^ ]+) /)?.[1] ?? "Illuminate"; + const saspyHtmlStyle = saspyGetHtmlStyleValue() ?? "Illuminate"; const cfgname = this._config.cfgname?.length > 0 ? this._config.cfgname : ""; @@ -233,7 +229,7 @@ ${codeWithEnd} */ public cancel = async () => { this._pollingForLogResults = false; - this._shellProcess.stdin.write("print(r'abc')\n", async (error) => { + this._shellProcess.stdin.write("%abort cancel;\n", async (error) => { if (error) { this._runReject(error); } diff --git a/client/src/connection/saspy/util.ts b/client/src/connection/saspy/util.ts new file mode 100644 index 000000000..4a52c57a4 --- /dev/null +++ b/client/src/connection/saspy/util.ts @@ -0,0 +1,33 @@ +// Copyright © 2024, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import { + ColorThemeKind, + window, +} from "vscode"; + +import { getHtmlStyle } from "../../components/utils/settings"; + +// To change the html style of SASPy +export const saspyGetHtmlStyleValue = (): string => { + const htmlStyleSetting = getHtmlStyle(); + + switch (htmlStyleSetting) { + case "(auto)": + switch (window.activeColorTheme.kind) { + case ColorThemeKind.Light: + return "Illuminate"; + case ColorThemeKind.Dark: + return "Ignite"; + case ColorThemeKind.HighContrast: + return "HighContrast"; + case ColorThemeKind.HighContrastLight: + return "Illuminate"; + default: + return ""; + } + case "(server default)": + return ""; + default: + return htmlStyleSetting; + } +} From 82da5a3b58ce5f530241a8826380aaa96c90b817 Mon Sep 17 00:00:00 2001 From: Shuguang Sun Date: Thu, 23 May 2024 17:48:17 +0800 Subject: [PATCH 08/17] refactor(saspy): refactor workdir Signed-off-by: Shuguang Sun --- client/src/connection/saspy/index.ts | 78 +++++++++++++++++++++------- 1 file changed, 58 insertions(+), 20 deletions(-) diff --git a/client/src/connection/saspy/index.ts b/client/src/connection/saspy/index.ts index 8bf9c197b..d11bc1afb 100644 --- a/client/src/connection/saspy/index.ts +++ b/client/src/connection/saspy/index.ts @@ -10,6 +10,11 @@ import { updateStatusBarItem } from "../../components/StatusBarItem"; import { Session } from "../session"; import { extractOutputHtmlFileName } from "../util"; // import { scriptContent } from "./script"; +import { LineParser } from "../itc/LineParser"; +import { + WORK_DIR_END_TAG, + WORK_DIR_START_TAG, +} from "../itc/const"; import { Config, LineCodes } from "./types"; import { saspyGetHtmlStyleValue } from "./util"; @@ -37,6 +42,16 @@ export class SASPYSession extends Session { private _workDirectory: string; private _pollingForLogResults: boolean; private _logLineType = 0; + private _workDirectoryParser: LineParser; + + constructor() { + super(); + this._workDirectoryParser = new LineParser( + WORK_DIR_START_TAG, + WORK_DIR_END_TAG, + false, + ); + } public set config(value: Config) { this._config = value; @@ -73,9 +88,11 @@ export class SASPYSession extends Session { this._shellProcess.stdout.on("data", this.onShellStdOut); this._shellProcess.stderr.on("data", this.onShellStdErr); const saspyWorkDir = ` -%let workDir = %sysfunc(pathname(work)); -%put &=workDir; -%let rc = %sysfunc(dlgcdir("&workDir")); +%let __workDir = %sysfunc(pathname(work)); +%put ${WORK_DIR_START_TAG}; +%put &__workDir.; +%put ${WORK_DIR_END_TAG}; +%let rc = %sysfunc(dlgcdir("&__workDir")); run; `; const saspyHtmlStyle = saspyGetHtmlStyleValue() ?? "Illuminate"; @@ -119,6 +136,7 @@ ${saspyWorkDir} """ ll=sas.submit(vscode_saspy_code) +print(ll['LOG']) `; @@ -134,7 +152,7 @@ ll=sas.submit(vscode_saspy_code) * will not exist. The work dir should only be deleted when close is invoked. */ if (!this._workDirectory) { - this._shellProcess.stdin.write(`sas\n`); + // this._shellProcess.stdin.write(`sas\n`); if (this._config.sasOptions?.length > 0) { const sasOptsInput = `$sasOpts=${this.formatSASOptions( @@ -189,12 +207,12 @@ ${codeWithEnd} this._pollingForLogResults = true; await this._shellProcess.stdin.write( `ll=sas.submit(codeToRun, results='HTML')\n`, + async (error) => { + await this.fetchLog(); if (error) { this._runReject(error); } - - await this.fetchLog(); }, ); @@ -282,6 +300,26 @@ ${codeWithEnd} } }; + private fetchWorkDirectory = (line: string): string | undefined => { + let foundWorkDirectory = ""; + if ( + !line.includes(`%put ${WORK_DIR_START_TAG};`) && + !line.includes(`%put &__workDir.;`) && + !line.includes(`%put ${WORK_DIR_END_TAG};`) + ) { + foundWorkDirectory = this._workDirectoryParser.processLine(line); + } else { + // If the line is the put statement, we don't need to log that + return; + } + // We don't want to output any of the captured lines + if (this._workDirectoryParser.isCapturingLine()) { + return; + } + + return foundWorkDirectory || ""; + }; + /** * Handles stdout output from the powershell child process. * @param data a buffer of stdout output from the child process. @@ -295,21 +333,21 @@ ${codeWithEnd} return; } - // if (!this._workDirectory && line.startsWith("WORKDIR=")) { - // const parts = line.split("WORKDIR="); - // this._workDirectory = parts[1].trim(); - // this._runResolve(); - // updateStatusBarItem(true); - // return; - // } - if (!this._workDirectory && /^WORK Path +=/.test(line)) { - const parts = line.split(/WORK Path +=/); - this._workDirectory = parts[1].trim(); - this._runResolve(); - updateStatusBarItem(true); - return; - } if (!this.processLineCodes(line)) { + if (!this._workDirectory) { + const foundWorkDirectory = this.fetchWorkDirectory(line); + if (foundWorkDirectory === undefined) { + return; + } + + if (foundWorkDirectory) { + this._workDirectory = foundWorkDirectory.trim(); + this._runResolve(); + updateStatusBarItem(true); + return; + } + } + this._html5FileName = extractOutputHtmlFileName( line, this._html5FileName, From 1bcf6100b5cc98f08346e218d8db33d73ced4e53 Mon Sep 17 00:00:00 2001 From: Shuguang Sun Date: Tue, 18 Jun 2024 18:04:50 +0800 Subject: [PATCH 09/17] chore(saspy): refactor doc as main branch Signed-off-by: Shuguang Sun --- .../Configurations/Profiles/additional.md | 14 ++++++++ .../docs/Configurations/Profiles/sas9py.md | 20 +++++++++++ website/docs/matrix.md | 34 +++++++++---------- 3 files changed, 51 insertions(+), 17 deletions(-) create mode 100644 website/docs/Configurations/Profiles/sas9py.md diff --git a/website/docs/Configurations/Profiles/additional.md b/website/docs/Configurations/Profiles/additional.md index ae342f992..1d9bbcf1c 100644 --- a/website/docs/Configurations/Profiles/additional.md +++ b/website/docs/Configurations/Profiles/additional.md @@ -65,6 +65,20 @@ SAS system options can be set for each connection profile. Changes to the SAS sy } ``` +- SAS 9.4 (SASPy): + + ```json + { + "profiles": { + "sas9SASPY": { + "cfgname": "winlocal", + "sasOptions": ["NONEWS", "ECHOAUTO", "PAGESIZE=MAX"], + "ConnectionType": "saspy" + } + } + } + ``` + ## SAS Autoexec Settings For SAS Viya connection profiles, you can set up autoexec code that executes each time you start a new session. Changes to the autoexec code do not take effect until you close and restart your SAS session. The Autoexec option supports different modes for how to define the SAS lines that should run: diff --git a/website/docs/Configurations/Profiles/sas9py.md b/website/docs/Configurations/Profiles/sas9py.md new file mode 100644 index 000000000..5d192b773 --- /dev/null +++ b/website/docs/Configurations/Profiles/sas9py.md @@ -0,0 +1,20 @@ +--- +sidebar_position: 5 +--- + +# SAS 9.4 (SASPy) Connection Profile + +To use SAS 9.4 (SASPy) connection type, you need to have Python and [SASPy](https://sassoftware.github.io/saspy) installed locally (the machine VS Code is installed on) or remotely (if you are using VScode remote development). SASPy fully supports connection methods of STDIO (Unix only), SSH, IOM (Remote and Local) and HTTP, and partially supports connection method of COM. You can visit the following [link](https://sassoftware.github.io/saspy) to find out how to install SASPy and how to configure it. + + +## Profile Anatomy + +A SAS 9.4 (SASPy) connection profile includes the following parameters: + +`"connectionType": "saspy"` + +| Name | Description | Additional Notes | +| -------------- | -------------- | ----------------------------------------------------------------------------- | +| `pythonpath` | Path to python | Defaults to `python` | +| `cfgname` | sascfg name | Visit [link](sassoftware.github.io/saspy/configuration.html) for introduction | + diff --git a/website/docs/matrix.md b/website/docs/matrix.md index 0f3b2c2a8..4e1589df7 100644 --- a/website/docs/matrix.md +++ b/website/docs/matrix.md @@ -4,23 +4,23 @@ sidebar_position: 2 # Capabilities by Connection Type -| SAS Extension for Visual Studio Code - Capabilities by Connection Type | SAS Viya | SAS 9.4 (local)
SAS 9.4 (remote - IOM) | SAS 9.4 (remote - SSH) | Notes | -| --------------------------------------------------------------------------------------------- | :----------------: | :------------------------------------------: | :--------------------: | -------------------------------------------------------------------- | -| [SAS Options settings](./Configurations/Profiles/additional.md#sas-options-settings-examples) | :heavy_check_mark: | :heavy_check_mark:\* | :heavy_check_mark: | \*Startup options not supported for SAS 9.4 (local) and (remote-IOM) | -| [SAS Autoexec settings](./Configurations/Profiles/additional.md#sas-autoexec-settings) | :heavy_check_mark: | :x: | :x: | -| [Access SAS Content](./Features/accessContent.md) | :heavy_check_mark: | :x: | :x: | -| [Access connected libraries](./Features/accessLibraries.md) | :heavy_check_mark: | :heavy_check_mark: | :x: | -| [Table viewer](./Features/accessLibraries.md) | :heavy_check_mark: | :heavy_check_mark: | :x: | -| [SAS Notebooks](./Features/sasNotebook.md) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | -| Convert SAS Notebook to SAS Studio Flow | :heavy_check_mark: | :x: | :x: | -| [SAS syntax highlighting in SAS code](./Features/sasCodeEditing.md#sas-syntax-highlighting) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | -| SAS errors, warnings, and notes highlighting in SAS log | :heavy_check_mark: | :heavy_check_mark: | :x: | A SAS color theme is required. | -| Show problems from SAS log | :heavy_check_mark: | :heavy_check_mark: | :x: | -| [Code folding and code outline](./Features/sasCodeEditing.md#code-folding-and-code-outline) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | -| [Code completion](./Features/sasCodeEditing.md#code-completion) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | -| [Pop-up syntax help](./Features/sasCodeEditing.md#pop-up-syntax-help) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | -| [Snippets](./Features/sasCodeEditing.md#snippets) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | -| Able to cancel a running program | :heavy_check_mark: | :heavy_check_mark: | :x: | +| SAS Extension for Visual Studio Code - Capabilities by Connection Type | SAS Viya | SAS 9.4 (local)
SAS 9.4 (remote - IOM) | SAS 9.4 (remote - SSH) | SAS 9.4 (remote - SSH) | Notes | +| --------------------------------------------------------------------------------------------- | :----------------: | :------------------------------------------: | :--------------------: | :---------------------: | -------------------------------------------------------------------- | +| [SAS Options settings](./Configurations/Profiles/additional.md#sas-options-settings-examples) | :heavy_check_mark: | :heavy_check_mark:\* | :heavy_check_mark: | :heavy_check_mark:\* | \*Startup options not supported for SAS 9.4 (local) and (remote-IOM) | +| [SAS Autoexec settings](./Configurations/Profiles/additional.md#sas-autoexec-settings) | :heavy_check_mark: | :x: | :x: | :x: | | +| [Access SAS Content](./Features/accessContent.md) | :heavy_check_mark: | :x: | :x: | :x: | | +| [Access connected libraries](./Features/accessLibraries.md) | :heavy_check_mark: | :heavy_check_mark: | :x: | :heavy_check_mark: | | +| [Table viewer](./Features/accessLibraries.md) | :heavy_check_mark: | :heavy_check_mark: | :x: | :heavy_check_mark: | | +| [SAS Notebooks](./Features/sasNotebook.md) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | +| Convert SAS Notebook to SAS Studio Flow | :heavy_check_mark: | :x: | :x: | | | +| [SAS syntax highlighting in SAS code](./Features/sasCodeEditing.md#sas-syntax-highlighting) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | +| SAS errors, warnings, and notes highlighting in SAS log | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | A SAS color theme is required. | +| Show problems from SAS log | :heavy_check_mark: | :heavy_check_mark: | :x: | :x: | | +| [Code folding and code outline](./Features/sasCodeEditing.md#code-folding-and-code-outline) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | +| [Code completion](./Features/sasCodeEditing.md#code-completion) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | +| [Pop-up syntax help](./Features/sasCodeEditing.md#pop-up-syntax-help) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | +| [Snippets](./Features/sasCodeEditing.md#snippets) | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | +| Able to cancel a running program | :heavy_check_mark: | :heavy_check_mark: | :x: | :heavy_check_mark: | | # Choose Connection Type From 829ad69553a64ccf0e3b2af34ac21b64a5164e36 Mon Sep 17 00:00:00 2001 From: Shuguang Sun Date: Tue, 6 Aug 2024 09:05:25 +0800 Subject: [PATCH 10/17] feat(saspy): enable problem diagnostics Signed-off-by: Shuguang Sun --- client/src/connection/saspy/index.ts | 79 ++++++++++++++++++++++++---- client/src/connection/saspy/types.ts | 1 + 2 files changed, 70 insertions(+), 10 deletions(-) diff --git a/client/src/connection/saspy/index.ts b/client/src/connection/saspy/index.ts index d11bc1afb..98dd00583 100644 --- a/client/src/connection/saspy/index.ts +++ b/client/src/connection/saspy/index.ts @@ -31,6 +31,20 @@ const LogLineTypes: LogLineTypeEnum[] = [ "message", ]; +// from SASpy +const LogLineTypeArray: string[] = [ + "Normal", + "Hilighted", + "Source", + "Title", + "Byline", + "Footnote", + "Error", + "Warning", + "Note", + "Message", +]; + let sessionInstance: SASPYSession; export class SASPYSession extends Session { @@ -43,7 +57,7 @@ export class SASPYSession extends Session { private _pollingForLogResults: boolean; private _logLineType = 0; private _workDirectoryParser: LineParser; - + constructor() { super(); this._workDirectoryParser = new LineParser( @@ -125,7 +139,7 @@ else: try: - sas + not sas except NameError: raise Exception("Setup error") @@ -135,8 +149,10 @@ vscode_saspy_code = r""" ${saspyWorkDir} """ -ll=sas.submit(vscode_saspy_code) -print(ll['LOG']) +ll_init=sas.submit(vscode_saspy_code) +if ll_init is not None: + print(ll_init['LOG']) + ll_init = None `; @@ -154,6 +170,15 @@ print(ll['LOG']) if (!this._workDirectory) { // this._shellProcess.stdin.write(`sas\n`); + // FIXME: Logically, the code for workdirectory should be here + // this._shellProcess.stdin.write(` + // ll_init=sas.submit(vscode_saspy_code) + // if ll_init is not None: + // print(ll_init['LOG']) + // ll_init = None + + // `, this.onWriteComplete); + if (this._config.sasOptions?.length > 0) { const sasOptsInput = `$sasOpts=${this.formatSASOptions( this._config.sasOptions, @@ -206,7 +231,10 @@ ${codeWithEnd} this._shellProcess.stdin.write(codeToRun); this._pollingForLogResults = true; await this._shellProcess.stdin.write( - `ll=sas.submit(codeToRun, results='HTML')\n`, + // Below SASPy V5.14.0, we can't get the log line type + // `ll=sas.submit(codeToRun, results='HTML')\n`, + // from SASPy V5.14.0, it provides an option to get line type in log + `ll=sas.submit(codeToRun, results='HTML', loglines=True)\n`, async (error) => { await this.fetchLog(); @@ -247,7 +275,7 @@ ${codeWithEnd} */ public cancel = async () => { this._pollingForLogResults = false; - this._shellProcess.stdin.write("%abort cancel;\n", async (error) => { + this._shellProcess.stdin.write(`sas.submit("""%abort cancel;\n""")\n`, async (error) => { if (error) { this._runReject(error); } @@ -272,7 +300,26 @@ ${codeWithEnd} * writing each chunk to stdout. */ private fetchLog = async (): Promise => { - this._shellProcess.stdin.write(`print(ll['LOG'])\n`, this.onWriteComplete); + // Below SASPy V5.14.0, we can't get the log line type + // this._shellProcess.stdin.write(`print(ll['LOG'])\n`, this.onWriteComplete); + // from SASPy V5.14.0, it provides an option to get line type in log + // FIXME: The log of code for work directory should be diagnoticed together with + // the first run code, otherwise, as current implentation, the diagnotitics would + // think the actual code has completed after parsing the log of code for working + // directory + // - update unsubscribe, or + // - delay the parsing log of code for working directory + this._shellProcess.stdin.write( + ` +if ll_init is not None: + print(ll_init['LOG']) + ll_init = None + +for lln in ll["LOG"]: + print("${LineCodes.LogLineStarter}=", lln["type"], ":LINE=", lln["line"], sep="") + +`, + this.onWriteComplete); }; /** @@ -354,9 +401,9 @@ ${codeWithEnd} ); if (this._workDirectory) { - this._onExecutionLogFn?.([{ type: this.getLogLineType(), line }]); + this._onExecutionLogFn?.([{ type: this.getLogLineType(line), line: this.getLogLineLog(line) }]); } else { - this._onSessionLogFn?.([{ type: this.getLogLineType(), line }]); + this._onSessionLogFn?.([{ type: this.getLogLineType(line), line: this.getLogLineLog(line) }]); } } }); @@ -389,12 +436,24 @@ ${codeWithEnd} return false; } - private getLogLineType(): LogLineTypeEnum { + private getLogLineType(line: string): LogLineTypeEnum { + this._logLineType = 0; + const rx: RegExp = /^--vscode-sas-extension-log-line-starter--=(\w+):LINE=.*/i; + if (rx.test(line)) { + const lineType = line.match(rx); + this._logLineType = LogLineTypeArray.indexOf(lineType[1]); + } const result = LogLineTypes[this._logLineType]; this._logLineType = 0; return result; } + private getLogLineLog(line: string): string { + const rx: RegExp = /^--vscode-sas-extension-log-line-starter--=\w+:LINE=(.*)/i; + const result = rx.test(line) ? line.match(rx)[1] : line; + return result; + } + /** * Generic call for use on stdin write completion. * @param err The error encountered on the write attempt. Undefined if no error occurred. diff --git a/client/src/connection/saspy/types.ts b/client/src/connection/saspy/types.ts index 5e044a652..96db6d4b2 100644 --- a/client/src/connection/saspy/types.ts +++ b/client/src/connection/saspy/types.ts @@ -8,6 +8,7 @@ export enum LineCodes { RunEndCode = "--vscode-sas-extension-submit-end--", SessionCreatedCode = "--vscode-sas-extension-session-created--", LogLineType = "--vscode-sas-extension-log-line-type--", + LogLineStarter = "--vscode-sas-extension-log-line-starter--", } /** From 5be205db35e3a9634be580920da3452d74b8548a Mon Sep 17 00:00:00 2001 From: Shuguang Sun Date: Tue, 6 Aug 2024 10:15:55 +0800 Subject: [PATCH 11/17] feat(saspy): check SASPy version when enabaling diagnostics Signed-off-by: Shuguang Sun --- client/src/connection/saspy/index.ts | 30 +++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/client/src/connection/saspy/index.ts b/client/src/connection/saspy/index.ts index 98dd00583..04c2f7e99 100644 --- a/client/src/connection/saspy/index.ts +++ b/client/src/connection/saspy/index.ts @@ -115,6 +115,7 @@ run; this._config.cfgname?.length > 0 ? this._config.cfgname : ""; const scriptContent = ` import saspy +from packaging.version import parse _cfgname = "${cfgname}" @@ -139,10 +140,13 @@ else: try: - not sas + sas except NameError: raise Exception("Setup error") +enable_diagnostic = parse(saspy.__version__) >= parse("5.14.0") +enable_diagnostic + sas.HTML_Style = '${saspyHtmlStyle}' vscode_saspy_code = r""" @@ -154,6 +158,8 @@ if ll_init is not None: print(ll_init['LOG']) ll_init = None +print("${LineCodes.SessionCreatedCode}") + `; this._shellProcess.stdin.write(scriptContent + "\n", this.onWriteComplete); @@ -177,7 +183,7 @@ if ll_init is not None: // print(ll_init['LOG']) // ll_init = None - // `, this.onWriteComplete); + // `, this.onWriteComplete); if (this._config.sasOptions?.length > 0) { const sasOptsInput = `$sasOpts=${this.formatSASOptions( @@ -225,8 +231,6 @@ ${codeWithEnd} """ `; - // console.log("codeToRun = " + codeToRun); - this._html5FileName = ""; this._shellProcess.stdin.write(codeToRun); this._pollingForLogResults = true; @@ -234,8 +238,13 @@ ${codeWithEnd} // Below SASPy V5.14.0, we can't get the log line type // `ll=sas.submit(codeToRun, results='HTML')\n`, // from SASPy V5.14.0, it provides an option to get line type in log - `ll=sas.submit(codeToRun, results='HTML', loglines=True)\n`, + ` +if enable_diagnostic: + ll=sas.submit(codeToRun, results='HTML', loglines=True) +else: + ll=sas.submit(codeToRun, results='HTML') +`, async (error) => { await this.fetchLog(); if (error) { @@ -303,8 +312,8 @@ ${codeWithEnd} // Below SASPy V5.14.0, we can't get the log line type // this._shellProcess.stdin.write(`print(ll['LOG'])\n`, this.onWriteComplete); // from SASPy V5.14.0, it provides an option to get line type in log - // FIXME: The log of code for work directory should be diagnoticed together with - // the first run code, otherwise, as current implentation, the diagnotitics would + // FIXME: The log of code for work directory should be diagnoticed together with + // the first run code, otherwise, as current implentation, the diagnotitics would // think the actual code has completed after parsing the log of code for working // directory // - update unsubscribe, or @@ -315,8 +324,11 @@ if ll_init is not None: print(ll_init['LOG']) ll_init = None -for lln in ll["LOG"]: - print("${LineCodes.LogLineStarter}=", lln["type"], ":LINE=", lln["line"], sep="") +if enable_diagnostic: + for lln in ll["LOG"]: + print("${LineCodes.LogLineStarter}=", lln["type"], ":LINE=", lln["line"], sep="") +else: + print(ll['LOG']) `, this.onWriteComplete); From 202e6f18cc59a1b1975ae815c4cd4694522b08cd Mon Sep 17 00:00:00 2001 From: Shuguang Sun Date: Thu, 8 Aug 2024 11:07:47 +0800 Subject: [PATCH 12/17] fix(saspy): using LineCodes in regex Signed-off-by: Shuguang Sun --- client/src/connection/saspy/index.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/client/src/connection/saspy/index.ts b/client/src/connection/saspy/index.ts index 04c2f7e99..829534e20 100644 --- a/client/src/connection/saspy/index.ts +++ b/client/src/connection/saspy/index.ts @@ -163,10 +163,6 @@ print("${LineCodes.SessionCreatedCode}") `; this._shellProcess.stdin.write(scriptContent + "\n", this.onWriteComplete); - // this._shellProcess.stdin.write( - // "$runner = New-Object -TypeName SASRunner\n", - // this.onWriteComplete, - // ); /* * There are cases where the higher level run command will invoke setup multiple times. @@ -222,6 +218,7 @@ print("${LineCodes.SessionCreatedCode}") /\bods html5\(id=vscode\)([^;]*;)/i, `ods html5(id=vscode) path="${this._workDirectory}"$1`, ); + // Remove ods close code from SASPy const codeWithODSPath2 = codeWithODSPath.replace(/\bods _all_ close;/i, ``); //write an end mnemonic so that the handler knows when execution has finished @@ -428,6 +425,10 @@ else: return true; } + if (line.includes(LineCodes.SessionCreatedCode)) { + return true; + } + if (line.includes(LineCodes.ResultsFetchedCode)) { this.displayResults(); return true; @@ -450,7 +451,7 @@ else: private getLogLineType(line: string): LogLineTypeEnum { this._logLineType = 0; - const rx: RegExp = /^--vscode-sas-extension-log-line-starter--=(\w+):LINE=.*/i; + const rx: RegExp = new RegExp(`^${LineCodes.LogLineStarter}=(\\w+):LINE=.*i`, 'i'); if (rx.test(line)) { const lineType = line.match(rx); this._logLineType = LogLineTypeArray.indexOf(lineType[1]); @@ -461,7 +462,7 @@ else: } private getLogLineLog(line: string): string { - const rx: RegExp = /^--vscode-sas-extension-log-line-starter--=\w+:LINE=(.*)/i; + const rx: RegExp = new RegExp(`^${LineCodes.LogLineStarter}=\\w+:LINE=(.*)`, 'i'); const result = rx.test(line) ? line.match(rx)[1] : line; return result; } From 5dc9c9d295a9593e3c201e25ae6d75e57eb46351 Mon Sep 17 00:00:00 2001 From: Shuguang Sun Date: Fri, 30 Aug 2024 21:08:57 +0800 Subject: [PATCH 13/17] refact(saspy): update according to ITC Signed-off-by: Shuguang Sun --- client/src/connection/saspy/CodeRunner.ts | 116 ++++++++++-------- .../connection/saspy/SaspyLibraryAdapter.ts | 13 +- 2 files changed, 69 insertions(+), 60 deletions(-) diff --git a/client/src/connection/saspy/CodeRunner.ts b/client/src/connection/saspy/CodeRunner.ts index 8cbc41e10..9ae3881ab 100644 --- a/client/src/connection/saspy/CodeRunner.ts +++ b/client/src/connection/saspy/CodeRunner.ts @@ -6,67 +6,77 @@ import { SASPYSession } from "."; import { LogLine, getSession } from ".."; import { useRunStore } from "../../store"; -class CodeRunner { - public async runCode( - code: string, - startTag: string = "", - endTag: string = "", - ): Promise { - // If we're already executing code, lets wait for it - // to finish up. - let unsubscribe; - if (useRunStore.getState().isExecutingCode) { - await new Promise((resolve) => { - unsubscribe = useRunStore.subscribe( - (state) => state.isExecutingCode, - (isExecutingCode) => !isExecutingCode && resolve(true), - ); - }); - } +let wait: Promise | undefined; - const { setIsExecutingCode } = useRunStore.getState(); - setIsExecutingCode(true); - commands.executeCommand("setContext", "SAS.running", true); - const session = getSession(); +export async function runCode( + code: string, + startTag: string = "", + endTag: string = "", +): Promise { + const task = () => _runCode(code, startTag, endTag); - let logText = ""; - const onExecutionLogFn = session.onExecutionLogFn; - const outputLines = []; - const addLine = (logLines: LogLine[]) => - outputLines.push(...logLines.map(({ line }) => line)); + wait = wait ? wait.then(task) : task(); + return wait; +} + +async function _runCode( + code: string, + startTag: string = "", + endTag: string = "", +): Promise { + // If we're already executing code, lets wait for it + // to finish up. + let unsubscribe; + if (useRunStore.getState().isExecutingCode) { + await new Promise((resolve) => { + unsubscribe = useRunStore.subscribe( + (state) => state.isExecutingCode, + (isExecutingCode) => !isExecutingCode && resolve(true), + ); + }); + } + + const { setIsExecutingCode } = useRunStore.getState(); + setIsExecutingCode(true); + commands.executeCommand("setContext", "SAS.running", true); + const session = getSession(); - try { - await session.setup(true); + let logText = ""; + const onExecutionLogFn = session.onExecutionLogFn; + const outputLines = []; - // Lets capture output to use it on - session.onExecutionLogFn = addLine; + const addLine = (logLines: LogLine[]) => + outputLines.push(...logLines.map(({ line }) => line)); - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - await (session as SASPYSession).run(code); + try { + await session.setup(true); - const logOutput = outputLines.filter((line) => line.trim()).join(""); + // Lets capture output to use it on + session.onExecutionLogFn = addLine; - logText = - startTag && endTag - ? logOutput - .slice( - logOutput.lastIndexOf(startTag), - logOutput.lastIndexOf(endTag), - ) - .replace(startTag, "") - .replace(endTag, "") - : logOutput; - } finally { - unsubscribe && unsubscribe(); - // Lets update our session to write to the log - session.onExecutionLogFn = onExecutionLogFn; + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + await (session as SASPYSession).run(code) - setIsExecutingCode(false); - commands.executeCommand("setContext", "SAS.running", false); - } + const logOutput = outputLines.filter((line) => line.trim()).join(""); - return logText; + logText = + startTag && endTag + ? logOutput + .slice( + logOutput.lastIndexOf(startTag), + logOutput.lastIndexOf(endTag), + ) + .replace(startTag, "") + .replace(endTag, "") + : logOutput; + } finally { + unsubscribe && unsubscribe(); + // Lets update our session to write to the log + session.onExecutionLogFn = onExecutionLogFn; + + setIsExecutingCode(false); + commands.executeCommand("setContext", "SAS.running", false); } -} -export default CodeRunner; + return logText; +} diff --git a/client/src/connection/saspy/SaspyLibraryAdapter.ts b/client/src/connection/saspy/SaspyLibraryAdapter.ts index 3eeb88f1b..edf2f270a 100644 --- a/client/src/connection/saspy/SaspyLibraryAdapter.ts +++ b/client/src/connection/saspy/SaspyLibraryAdapter.ts @@ -12,7 +12,7 @@ import { TableRow, } from "../../components/LibraryNavigator/types"; import { Column, ColumnCollection } from "../rest/api/compute"; -import CodeRunner from "./CodeRunner"; +import { runCode } from "./CodeRunner"; import { Config } from "./types"; class SaspyLibraryAdapter implements LibraryAdapter { @@ -23,7 +23,6 @@ class SaspyLibraryAdapter implements LibraryAdapter { protected endTag: string = ""; protected outputFinished: boolean = false; protected config: Config; - protected codeRunner = new CodeRunner(); public async connect(): Promise { this.hasEstablishedConnection = true; @@ -54,7 +53,7 @@ class SaspyLibraryAdapter implements LibraryAdapter { where libname='${item.library}' and memname='${item.name}' order by varnum; quit; - %put ; %put &OUTPUT; %put ; + %put &OUTPUT; %put ; `; const columnLines = processQueryRows( @@ -87,7 +86,7 @@ class SaspyLibraryAdapter implements LibraryAdapter { select catx(',', libname, readonly) as libname_target into: OUTPUT separated by '~' from sashelp.vlibnam order by libname asc; quit; - %put ; %put &OUTPUT; %put ; + %put &OUTPUT; %put ; `; const libNames = processQueryRows( @@ -186,7 +185,7 @@ class SaspyLibraryAdapter implements LibraryAdapter { where libname='${item.name!}' order by memname asc; quit; - %put ; %put &OUTPUT; %put ; + %put &OUTPUT; %put ; `; const tableNames = processQueryRows( @@ -249,7 +248,7 @@ class SaspyLibraryAdapter implements LibraryAdapter { const count = parseInt(countMatches[1].replace(/\s|\n/gm, ""), 10); output = output.replace(countRegex, ""); - const rows = output.replace(/\n|\t/gm, ""); + const rows = output.replace(/\n|\t/gm, "").slice(output.indexOf("{")); try { const tableData = JSON.parse(rows); return { rows: tableData[`SASTableData+${tempTable}`], count }; @@ -270,7 +269,7 @@ class SaspyLibraryAdapter implements LibraryAdapter { endTag: string = "", ): Promise { try { - return await this.codeRunner.runCode(code, startTag, endTag); + return await runCode(code, startTag, endTag); } catch (e) { onRunError(e); commands.executeCommand("setContext", "SAS.librariesDisplayed", false); From 953e65909c22d50d9056b11dd3b77adfed5ab39c Mon Sep 17 00:00:00 2001 From: Shuguang Sun Date: Fri, 30 Aug 2024 23:44:50 +0800 Subject: [PATCH 14/17] fix(saspy): remove LogLineStarter from json string Signed-off-by: Shuguang Sun --- client/src/connection/saspy/SaspyLibraryAdapter.ts | 6 +++++- client/src/connection/saspy/index.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/client/src/connection/saspy/SaspyLibraryAdapter.ts b/client/src/connection/saspy/SaspyLibraryAdapter.ts index edf2f270a..28e6a4e9b 100644 --- a/client/src/connection/saspy/SaspyLibraryAdapter.ts +++ b/client/src/connection/saspy/SaspyLibraryAdapter.ts @@ -13,7 +13,7 @@ import { } from "../../components/LibraryNavigator/types"; import { Column, ColumnCollection } from "../rest/api/compute"; import { runCode } from "./CodeRunner"; -import { Config } from "./types"; +import { Config, LineCodes } from "./types"; class SaspyLibraryAdapter implements LibraryAdapter { protected hasEstablishedConnection: boolean = false; @@ -242,6 +242,10 @@ class SaspyLibraryAdapter implements LibraryAdapter { let output = await this.runCode(code, "", ""); + // Remove LogLineStarter + const rxLineType: RegExp = new RegExp(`${LineCodes.LogLineStarter}=(\\w+):LINE=`, 'igm'); + output = output.replace(rxLineType, ""); + // Extract result count const countRegex = /(.*)<\/Count>/; const countMatches = output.match(countRegex); diff --git a/client/src/connection/saspy/index.ts b/client/src/connection/saspy/index.ts index 829534e20..a36673464 100644 --- a/client/src/connection/saspy/index.ts +++ b/client/src/connection/saspy/index.ts @@ -281,7 +281,7 @@ else: */ public cancel = async () => { this._pollingForLogResults = false; - this._shellProcess.stdin.write(`sas.submit("""%abort cancel;\n""")\n`, async (error) => { + this._shellProcess.stdin.write(`sas.submit("""\n%abort cancel;\n""")\n`, async (error) => { if (error) { this._runReject(error); } From 005af2c917ecdd09c30b18ded1382fb4f4749912 Mon Sep 17 00:00:00 2001 From: Shuguang Sun Date: Wed, 4 Sep 2024 00:12:16 +0800 Subject: [PATCH 15/17] chore(saspy): add newline to end of log Signed-off-by: Shuguang Sun --- client/src/connection/saspy/SaspyLibraryAdapter.ts | 2 +- client/src/connection/saspy/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/connection/saspy/SaspyLibraryAdapter.ts b/client/src/connection/saspy/SaspyLibraryAdapter.ts index 28e6a4e9b..52ccbffd0 100644 --- a/client/src/connection/saspy/SaspyLibraryAdapter.ts +++ b/client/src/connection/saspy/SaspyLibraryAdapter.ts @@ -236,7 +236,7 @@ class SaspyLibraryAdapter implements LibraryAdapter { %put &COUNT; data _null_; infile out; input; put _infile_; run; %put
; - proc datasets library=work nolist nodetails; delete ${tempTable}; run; + proc datasets library=work nolist nodetails; delete ${tempTable}; run; quit; options notes source date number; `; diff --git a/client/src/connection/saspy/index.ts b/client/src/connection/saspy/index.ts index a36673464..904980445 100644 --- a/client/src/connection/saspy/index.ts +++ b/client/src/connection/saspy/index.ts @@ -323,7 +323,7 @@ if ll_init is not None: if enable_diagnostic: for lln in ll["LOG"]: - print("${LineCodes.LogLineStarter}=", lln["type"], ":LINE=", lln["line"], sep="") + print("${LineCodes.LogLineStarter}=", lln["type"], ":LINE=", lln["line"], sep="", end='\\n') else: print(ll['LOG']) From 801b1d257c2822820cb7cd3fc1e4cd9807b9942f Mon Sep 17 00:00:00 2001 From: Shuguang Sun Date: Wed, 27 Nov 2024 09:17:06 +0800 Subject: [PATCH 16/17] fix: adapt change as ITC except workdirectory Signed-off-by: Shuguang Sun --- client/src/components/profile.ts | 1 + client/src/connection/saspy/CodeRunner.ts | 2 +- client/src/connection/saspy/index.ts | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/client/src/components/profile.ts b/client/src/components/profile.ts index 155e2a1ea..01a599fdc 100644 --- a/client/src/components/profile.ts +++ b/client/src/components/profile.ts @@ -848,6 +848,7 @@ const input: ProfilePromptInput = { title: l10n.t("Server Path"), placeholder: l10n.t("Enter the server path"), description: l10n.t("Enter the server path of the PYTHON Executable."), + } }; /** diff --git a/client/src/connection/saspy/CodeRunner.ts b/client/src/connection/saspy/CodeRunner.ts index 9ae3881ab..eaf9ebede 100644 --- a/client/src/connection/saspy/CodeRunner.ts +++ b/client/src/connection/saspy/CodeRunner.ts @@ -37,7 +37,7 @@ async function _runCode( } const { setIsExecutingCode } = useRunStore.getState(); - setIsExecutingCode(true); + setIsExecutingCode(true, false); commands.executeCommand("setContext", "SAS.running", true); const session = getSession(); diff --git a/client/src/connection/saspy/index.ts b/client/src/connection/saspy/index.ts index 904980445..249db4601 100644 --- a/client/src/connection/saspy/index.ts +++ b/client/src/connection/saspy/index.ts @@ -181,6 +181,8 @@ print("${LineCodes.SessionCreatedCode}") // `, this.onWriteComplete); + this._workDirectoryParser.reset(); + if (this._config.sasOptions?.length > 0) { const sasOptsInput = `$sasOpts=${this.formatSASOptions( this._config.sasOptions, @@ -368,6 +370,7 @@ else: // If the line is the put statement, we don't need to log that return; } + // const foundWorkDirectory = this._workDirectoryParser.processLine(line); // We don't want to output any of the captured lines if (this._workDirectoryParser.isCapturingLine()) { return; @@ -496,6 +499,7 @@ else: const globalStorageUri = getGlobalStorageUri(); try { await workspace.fs.readDirectory(globalStorageUri); + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { await workspace.fs.createDirectory(globalStorageUri); } From 60f2a368a8b1be7d712be1c9cf06fe3214bf5290 Mon Sep 17 00:00:00 2001 From: Shuguang Sun Date: Mon, 23 Dec 2024 12:55:22 +0800 Subject: [PATCH 17/17] fix: using session Signed-off-by: Shuguang Sun --- client/src/connection/saspy/CodeRunner.ts | 2 +- client/src/connection/saspy/index.ts | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/client/src/connection/saspy/CodeRunner.ts b/client/src/connection/saspy/CodeRunner.ts index eaf9ebede..8e8b6bbb9 100644 --- a/client/src/connection/saspy/CodeRunner.ts +++ b/client/src/connection/saspy/CodeRunner.ts @@ -55,7 +55,7 @@ async function _runCode( session.onExecutionLogFn = addLine; // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - await (session as SASPYSession).run(code) + await (session as SASPYSession).run(code, true) const logOutput = outputLines.filter((line) => line.trim()).join(""); diff --git a/client/src/connection/saspy/index.ts b/client/src/connection/saspy/index.ts index 249db4601..c7224364f 100644 --- a/client/src/connection/saspy/index.ts +++ b/client/src/connection/saspy/index.ts @@ -209,7 +209,10 @@ print("${LineCodes.SessionCreatedCode}") * @param onLog A callback handler responsible for marshalling log lines back to the higher level extension API. * @returns A promise that eventually resolves to contain the given {@link RunResult} for the input code execution. */ - public run = async (code: string): Promise => { + public run = async ( + code: string, + skipPageHeaders?: boolean, + ): Promise => { const runPromise = new Promise((resolve, reject) => { this._runResolve = resolve; this._runReject = reject; @@ -218,7 +221,7 @@ print("${LineCodes.SessionCreatedCode}") //write ODS output to work so that the session cleans up after itself const codeWithODSPath = code.replace( /\bods html5\(id=vscode\)([^;]*;)/i, - `ods html5(id=vscode) path="${this._workDirectory}"$1`, + `ods html5(id=vscode) path="${this._workDirectory}" $1`, ); // Remove ods close code from SASPy const codeWithODSPath2 = codeWithODSPath.replace(/\bods _all_ close;/i, ``); @@ -245,10 +248,11 @@ else: `, async (error) => { - await this.fetchLog(); if (error) { this._runReject(error); } + + await this.fetchLog(skipPageHeaders); }, ); @@ -307,7 +311,7 @@ else: * Flushes the SAS log in chunks of [chunkSize] length, * writing each chunk to stdout. */ - private fetchLog = async (): Promise => { + private fetchLog = async (skipPageHeaders?: boolean): Promise => { // Below SASPy V5.14.0, we can't get the log line type // this._shellProcess.stdin.write(`print(ll['LOG'])\n`, this.onWriteComplete); // from SASPy V5.14.0, it provides an option to get line type in log @@ -317,6 +321,7 @@ else: // directory // - update unsubscribe, or // - delay the parsing log of code for working directory + const skipPageHeadersValue = skipPageHeaders ? "True" : "False"; this._shellProcess.stdin.write( ` if ll_init is not None: @@ -325,7 +330,11 @@ if ll_init is not None: if enable_diagnostic: for lln in ll["LOG"]: - print("${LineCodes.LogLineStarter}=", lln["type"], ":LINE=", lln["line"], sep="", end='\\n') + if ${skipPageHeadersValue}: + if lln["type"] != "Title": + print("${LineCodes.LogLineStarter}=", lln["type"], ":LINE=", lln["line"], sep="", end='\\n') + else: + print("${LineCodes.LogLineStarter}=", lln["type"], ":LINE=", lln["line"], sep="", end='\\n') else: print(ll['LOG'])