From ef7811c7be00f27dc898b1791df625e271b41aa3 Mon Sep 17 00:00:00 2001 From: Saad Jutt Date: Wed, 6 Oct 2021 20:06:31 +0500 Subject: [PATCH 01/12] chore: added https server for production --- package.json | 1 + src/prod-server.ts | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 src/prod-server.ts diff --git a/package.json b/package.json index 2981f52..459c4e6 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "./src/server.ts", "scripts": { "start": "nodemon ./src/server.ts", + "start:prod": "nodemon ./src/prod-server.ts", "build": "rimraf build && tsc", "semantic-release": "semantic-release -d", "prepare": "[ -d .git ] && git config core.hooksPath ./.git-hooks || true", diff --git a/src/prod-server.ts b/src/prod-server.ts new file mode 100644 index 0000000..e04c645 --- /dev/null +++ b/src/prod-server.ts @@ -0,0 +1,18 @@ +import path from 'path' +import { readFileSync } from 'fs' +import * as https from 'https' + +import app from './app' + +const port = 5001 +const keyPath = path.join('certificates', 'privkey.pem') +const certPath = path.join('certificates', 'fullchain.pem') + +const key = readFileSync(keyPath) +const cert = readFileSync(certPath) + +const httpsServer = https.createServer({ key, cert }, app) + +httpsServer.listen(port, () => { + console.log(`⚡️[server]: Server is running at https://localhost:${port}`) +}) From e73c21aa4d01c31d0b3a59d7d01ab873b12198c8 Mon Sep 17 00:00:00 2001 From: Yury Shkoda Date: Mon, 11 Oct 2021 09:38:46 +0300 Subject: [PATCH 02/12] chore: update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 94b3e31..f28d61e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# @sasjs/server +# SASjsServer ## Configuration -In order to configure `@sasjs/server`, add the following information to the `configuration` section of `package.json`: +In order to configure `SASjsServer`, add the following information to the `configuration` section of `package.json`: - Provide path to SAS9 executable. -- Provide `@sasjs/server` desired port. +- Provide `SASjsServer` desired port. From b3342f00031d19080fb72e3460f023c5f44bac95 Mon Sep 17 00:00:00 2001 From: Allan Bowe Date: Mon, 11 Oct 2021 10:52:41 +0000 Subject: [PATCH 03/12] fix: readme overview| --- README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f28d61e..213ec77 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,16 @@ -# SASjsServer +# SASjs Server + +SASjs Server provides a NodeJS wrapper for calling the SAS binary executable. It can be installed on an actual SAS server, or it could even run locally on your desktop. It provides the following functionality: + +* Virtual filesystem for storing SAS programs and other content +* Ability to execute Stored Programs from a URL +* Ability to create web apps using simple Desktop SAS + +One major benefit of using SASjs Server (alongside other components of the SASjs framework such as the [CLI](https://cli.sasjs.io), [Adapter](https://adapter.sasjs.io) and [Core](https://core.sasjs.io) library) is that the projects you create can be very easily ported to SAS 9 (Stored Process server) or Viya (Job Execution server). ## Configuration -In order to configure `SASjsServer`, add the following information to the `configuration` section of `package.json`: +Configuration is made in the `configuration` section of `package.json`: - Provide path to SAS9 executable. -- Provide `SASjsServer` desired port. +- Provide `SASjsServer` hostname and port (eg `localhost:5000`). From 6e0b04a6e548ac31baee726c9249b7e25f50f0bf Mon Sep 17 00:00:00 2001 From: Yury Shkoda Date: Mon, 11 Oct 2021 14:16:22 +0300 Subject: [PATCH 04/12] feat(session): add SessionController and ExecutionController --- .vscode/settings.json | 5 ++ src/controllers/executor.ts | 68 ++++++++++++++++++++ src/controllers/index.ts | 1 + src/controllers/sas.ts | 122 +++++++++++++++++++----------------- src/controllers/session.ts | 109 ++++++++++++++++++++++++++++++++ src/types/Process.d.ts | 5 ++ src/types/Session.ts | 7 +++ src/types/index.ts | 2 + src/utils/file.ts | 5 +- 9 files changed, 266 insertions(+), 58 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/controllers/executor.ts create mode 100644 src/controllers/session.ts create mode 100644 src/types/Process.d.ts create mode 100644 src/types/Session.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..fb07265 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cSpell.words": [ + "autoexec" + ] +} \ No newline at end of file diff --git a/src/controllers/executor.ts b/src/controllers/executor.ts new file mode 100644 index 0000000..db00297 --- /dev/null +++ b/src/controllers/executor.ts @@ -0,0 +1,68 @@ +import { getSessionController } from './session' +import { readFile, deleteFile, fileExists, createFile } from '@sasjs/utils' +import path from 'path' +import { configuration } from '../../package.json' +import { promisify } from 'util' +import { execFile } from 'child_process' +const execFilePromise = promisify(execFile) + +export class ExecutionController { + async execute(program = '', autoExec?: string, debug?: number) { + console.log(`[ExecutionController]program: `, program) + console.log(`[ExecutionController]autoExec: `, autoExec) + + if (program) { + if (!(await fileExists(program))) { + throw 'SASjsServer/Executor: SAS file does not exist.' + } + + program = await readFile(program) + } + + const sessionController = getSessionController() + const session = await sessionController.getSession() + + console.log(`[ExecutionController]session: `, session) + + let log = path.join(session.path, 'log.log') + await createFile(log, '') + + let webout = path.join(session.path, 'webout.txt') + await createFile(webout, '') + + const code = path.join(session.path, 'code') + await createFile(code, program) + + let additionalArgs: string[] = [] + if (autoExec) additionalArgs = ['-AUTOEXEC', autoExec] + + const { stdout, stderr } = await execFilePromise(configuration.sasPath, [ + '-SYSIN', + code, + '-LOG', + log, + '-WORK', + ...additionalArgs, + session.path, + process.platform === 'win32' ? '-nosplash' : '' + ]).catch((err) => ({ stderr: err, stdout: '' })) + + log = await readFile(log) + + if (stderr) return Promise.reject({ error: stderr, log: log }) + + webout = await readFile(webout) + + if (debug && debug >= 131) { + webout = ` +${webout} +
+

SAS Log

+
${log}
+
+` + } + + return Promise.resolve(webout) + } +} diff --git a/src/controllers/index.ts b/src/controllers/index.ts index e90f306..4df137d 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,2 +1,3 @@ export * from './sas' export * from './deploy' +export * from './session' diff --git a/src/controllers/sas.ts b/src/controllers/sas.ts index afdd4dc..e30e382 100644 --- a/src/controllers/sas.ts +++ b/src/controllers/sas.ts @@ -8,6 +8,7 @@ import { generateUniqueFileName } from '../utils' import { configuration } from '../../package.json' +import { SessionController } from './session' import { promisify } from 'util' import { execFile } from 'child_process' const execFilePromise = promisify(execFile) @@ -21,79 +22,86 @@ export const processSas = async (query: ExecutionQuery): Promise => { return Promise.reject({ error: 'SAS file does not exist.' }) } - const sasFile: string = sasCodePath.split(path.sep).pop() || 'default' + // FIXME + const sessionController = new SessionController() - const sasLogPath = path.join( - getTmpLogFolderPath(), - generateUniqueFileName(sasFile.replace(/\.sas/g, ''), '.log') - ) + sessionController.getSession() - await createFile(sasLogPath, '') + return Promise.resolve('success') - const sasWeboutPath = path.join( - getTmpWeboutFolderPath(), - generateUniqueFileName(sasFile.replace(/\.sas/g, ''), '.txt') - ) + // const sasFile: string = sasCodePath.split(path.sep).pop() || 'default' - await createFile(sasWeboutPath, '') + // const sasLogPath = path.join( + // getTmpLogFolderPath(), + // generateUniqueFileName(sasFile.replace(/\.sas/g, ''), '.log') + // ) - let sasCode = await readFile(sasCodePath) + // await createFile(sasLogPath, '') - const vars: any = query - Object.keys(query).forEach( - (key: string) => (sasCode = `%let ${key}=${vars[key]};\n${sasCode}`) - ) + // const sasWeboutPath = path.join( + // getTmpWeboutFolderPath(), + // generateUniqueFileName(sasFile.replace(/\.sas/g, ''), '.txt') + // ) - sasCode = `filename _webout "${sasWeboutPath}";\n${sasCode}` + // await createFile(sasWeboutPath, '') - const tmpSasCodePath = sasCodePath.replace( - sasFile, - generateUniqueFileName(sasFile) - ) + // let sasCode = await readFile(sasCodePath) - await createFile(tmpSasCodePath, sasCode) + // const vars: any = query + // Object.keys(query).forEach( + // (key: string) => (sasCode = `%let ${key}=${vars[key]};\n${sasCode}`) + // ) - const { stdout, stderr } = await execFilePromise(configuration.sasPath, [ - '-SYSIN', - tmpSasCodePath, - '-log', - sasLogPath, - process.platform === 'win32' ? '-nosplash' : '' - ]).catch((err) => ({ stderr: err, stdout: '' })) + // sasCode = `filename _webout "${sasWeboutPath}";\n${sasCode}` - let log = '' - if (sasLogPath && (await fileExists(sasLogPath))) { - log = await readFile(sasLogPath) - } + // const tmpSasCodePath = sasCodePath.replace( + // sasFile, + // generateUniqueFileName(sasFile) + // ) - await deleteFile(sasLogPath) - await deleteFile(tmpSasCodePath) + // await createFile(tmpSasCodePath, sasCode) - if (stderr) return Promise.reject({ error: stderr, log: log }) + // const { stdout, stderr } = await execFilePromise(configuration.sasPath, [ + // '-SYSIN', + // tmpSasCodePath, + // '-log', + // sasLogPath, + // process.platform === 'win32' ? '-nosplash' : '' + // ]).catch((err) => ({ stderr: err, stdout: '' })) - if (await fileExists(sasWeboutPath)) { - let webout = await readFile(sasWeboutPath) + // let log = '' + // if (sasLogPath && (await fileExists(sasLogPath))) { + // log = await readFile(sasLogPath) + // } - await deleteFile(sasWeboutPath) + // await deleteFile(sasLogPath) + // await deleteFile(tmpSasCodePath) - const debug = Object.keys(query).find( - (key: string) => key.toLowerCase() === '_debug' - ) + // if (stderr) return Promise.reject({ error: stderr, log: log }) - if (debug && (query as any)[debug] >= 131) { - webout = ` -${webout} -
-

SAS Log

-
${log}
-
-` - } + // if (await fileExists(sasWeboutPath)) { + // let webout = await readFile(sasWeboutPath) - return Promise.resolve(webout) - } else { - return Promise.resolve({ - log: log - }) - } + // await deleteFile(sasWeboutPath) + + // const debug = Object.keys(query).find( + // (key: string) => key.toLowerCase() === '_debug' + // ) + + // if (debug && (query as any)[debug] >= 131) { + // webout = ` + // ${webout} + //
+ //

SAS Log

+ //
${log}
+ //
+ // ` + // } + + // return Promise.resolve(webout) + // } else { + // return Promise.resolve({ + // log: log + // }) + // } } diff --git a/src/controllers/session.ts b/src/controllers/session.ts new file mode 100644 index 0000000..0b17c73 --- /dev/null +++ b/src/controllers/session.ts @@ -0,0 +1,109 @@ +import { Session } from '../types' +import { getTmpSessionsFolderPath, generateUniqueFileName } from '../utils' +import { createFolder, createFile, generateTimestamp } from '@sasjs/utils' +import path from 'path' +import { ExecutionController } from './executor' + +// /opt/sas/sas9/SASHome/SASFoundation/9.4/sasexe/sas +// -LOG /tmp/mydir/demo.log +// -SYSIN /tmp/mydir/code.sas +// -AUTOEXEC /tmp/mydir/autoexec.sas +// -WORK /tmp/mydir + +// 1. req (_program) for execution +// 2. check available session +// 2.3 spawn one more session +// 2.3.1 create folder +// 2.3.2 create autoexec +// 2.3.3 create _program.sas (empty) +// 2.3.4 /opt/sas/sas9/SASHome/SASFoundation/9.4/sasexe/sas -LOG /tmp/sessionFolder/demo.log -SYSIN /tmp/sessionFolder/_program.sas -AUTOEXEC /tmp/sessionFolder/autoexec.sas -WORK /tmp/sessionFolder +// 2.3.5 wait for _program.sas to be deleted +// 2.3.6 add session to the session array +// --- +// 3.0 wait for session +// 3.1 create _program.sas in sessionFolder +// 3.2 poll session array + +export class SessionController { + private sessions: Session[] = [] + private executionController: ExecutionController + + constructor() { + this.executionController = new ExecutionController() + } + + public async getSession() { + if (this.sessions.length) { + const session: Session = this.sessions[0] + + // TODO: check if session is not expired + + return session + } + + return await this.createSession() + } + + private async createSession() { + if (!this.sessions.length) { + const sessionId = generateUniqueFileName(generateTimestamp()) + + const sessionFolder = path.join( + await getTmpSessionsFolderPath(), + sessionId + ) + + const autoExecContent = `data _null_; + /* remove the dummy SYSIN */ + length fname $8; + rc=filename(fname,getoption('SYSIN') ); + if rc = 0 and fexist(fname) then rc=fdelete(fname); + rc=filename(fname); + /* now wait for the real SYSIN */ + slept=0; + do until ( fileexist(getoption('SYSIN')) or slept>(60*15) ); + slept=slept+sleep(0.1,1); + end; +run; +EOL` + + const autoExec = path.join(sessionFolder, 'autoexec.sas') + + await createFile(autoExec, autoExecContent) + + console.log(`[SessionController about to create first session]`) + + this.executionController.execute('', autoExec) + + const creationTimeStamp = sessionId.split('-').pop() as string + + const session: Session = { + id: sessionId, + available: true, + creationTimeStamp: creationTimeStamp, + deathTimeStamp: ( + parseInt(creationTimeStamp) + + 15 * 60 * 1000 - + 1000 + ).toString(), + path: sessionFolder + } + + console.log(`[SessionController]session: `, session) + + this.sessions.push(session) + + return session + } else { + return this.sessions[0] + } + } +} + +export const getSessionController = () => { + if (process.sessionController) return process.sessionController + + process.sessionController = new SessionController() + + return process.sessionController +} diff --git a/src/types/Process.d.ts b/src/types/Process.d.ts new file mode 100644 index 0000000..501fd16 --- /dev/null +++ b/src/types/Process.d.ts @@ -0,0 +1,5 @@ +declare namespace NodeJS { + export interface Process { + sessionController?: import('../controllers/session').SessionController + } +} diff --git a/src/types/Session.ts b/src/types/Session.ts new file mode 100644 index 0000000..9066363 --- /dev/null +++ b/src/types/Session.ts @@ -0,0 +1,7 @@ +export interface Session { + id: string + available: boolean + creationTimeStamp: string + deathTimeStamp: string + path: string +} diff --git a/src/types/index.ts b/src/types/index.ts index 7897c28..1c326b2 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,3 +1,5 @@ +// TODO: uppercase types export * from './sas' export * from './request' export * from './fileTree' +export * from './Session' diff --git a/src/utils/file.ts b/src/utils/file.ts index bd3f759..fe09816 100644 --- a/src/utils/file.ts +++ b/src/utils/file.ts @@ -12,12 +12,15 @@ export const getTmpLogFolderPath = () => path.join(getTmpFolderPath(), 'logs') export const getTmpWeboutFolderPath = () => path.join(getTmpFolderPath(), 'webouts') +export const getTmpSessionsFolderPath = () => + path.join(getTmpFolderPath(), 'sessions') + export const generateUniqueFileName = (fileName: string, extension = '') => [ fileName, '-', Math.round(Math.random() * 100000), '-', - generateTimestamp(), + new Date().getTime(), extension ].join('') From 37b6936cca3cff9c1ca26ec7b4b938a357c448df Mon Sep 17 00:00:00 2001 From: Yury Shkoda Date: Mon, 11 Oct 2021 14:35:47 +0300 Subject: [PATCH 05/12] fix(ts): enable files --- tsconfig.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tsconfig.json b/tsconfig.json index 50262e5..391d326 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,5 +7,8 @@ "esModuleInterop": true, "strict": true, "resolveJsonModule": true + }, + "ts-node": { + "files": true } } From 4d8efbb88d32154d84e80b79780e2e3de2f519e4 Mon Sep 17 00:00:00 2001 From: Yury Shkoda Date: Mon, 11 Oct 2021 16:42:17 +0300 Subject: [PATCH 06/12] fix(prod-server): use port from configuration --- src/prod-server.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/prod-server.ts b/src/prod-server.ts index e04c645..e652e39 100644 --- a/src/prod-server.ts +++ b/src/prod-server.ts @@ -1,10 +1,9 @@ import path from 'path' import { readFileSync } from 'fs' import * as https from 'https' - +import { configuration } from '../package.json' import app from './app' -const port = 5001 const keyPath = path.join('certificates', 'privkey.pem') const certPath = path.join('certificates', 'fullchain.pem') @@ -13,6 +12,8 @@ const cert = readFileSync(certPath) const httpsServer = https.createServer({ key, cert }, app) -httpsServer.listen(port, () => { - console.log(`⚡️[server]: Server is running at https://localhost:${port}`) +httpsServer.listen(configuration.sasJsPort, () => { + console.log( + `⚡️[server]: Server is running at https://localhost:${configuration.sasJsPort}` + ) }) From 8b2564120def137f80647064e28062b880d58efe Mon Sep 17 00:00:00 2001 From: Yury Shkoda Date: Thu, 14 Oct 2021 07:31:46 +0000 Subject: [PATCH 07/12] feat(execution): add ExecutionController working with session --- src/controllers/Execution.ts | 92 ++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/controllers/Execution.ts diff --git a/src/controllers/Execution.ts b/src/controllers/Execution.ts new file mode 100644 index 0000000..04f835f --- /dev/null +++ b/src/controllers/Execution.ts @@ -0,0 +1,92 @@ +import { getSessionController } from './' +import { readFile, fileExists, createFile } from '@sasjs/utils' +import path from 'path' +import { configuration } from '../../package.json' +import { promisify } from 'util' +import { execFile } from 'child_process' +import { Session } from '../types' +const execFilePromise = promisify(execFile) + +export class ExecutionController { + async execute( + program = '', + autoExec?: string, + session?: Session, + vars?: any + ) { + if (program) { + if (!(await fileExists(program))) { + throw 'ExecutionController: SAS file does not exist.' + } + + program = await readFile(program) + + if (vars) { + Object.keys(vars).forEach( + (key: string) => (program = `%let ${key}=${vars[key]};\n${program}`) + ) + } + } + + const sessionController = getSessionController() + + if (!session) { + session = await sessionController.getSession() + session.inUse = true + } + + let log = path.join(session.path, 'log.log') + + let webout = path.join(session.path, 'webout.txt') + await createFile(webout, '') + + program = `filename _webout "${webout}";\n${program}` + + const code = path.join(session.path, 'code.sas') + if (!(await fileExists(code))) { + await createFile(code, program) + } + + let additionalArgs: string[] = [] + if (autoExec) additionalArgs = ['-AUTOEXEC', autoExec] + + const { stdout, stderr } = await execFilePromise(configuration.sasPath, [ + '-SYSIN', + code, + '-LOG', + log, + '-WORK', + session.path, + ...additionalArgs, + process.platform === 'win32' ? '-nosplash' : '' + ]).catch((err) => ({ stderr: err, stdout: '' })) + + if (await fileExists(log)) log = await readFile(log) + else log = '' + + if (stderr) return Promise.reject({ error: stderr, log: log }) + + if (await fileExists(webout)) webout = await readFile(webout) + else webout = '' + + const debug = Object.keys(vars).find( + (key: string) => key.toLowerCase() === '_debug' + ) + + if (debug && vars[debug] >= 131) { + webout = ` +${webout} +
+

SAS Log

+
${log}
+
+` + } + + session.inUse = false + + sessionController.deleteSession(session) + + return Promise.resolve(webout) + } +} From 6a34fa1b1dae07fe032352bea0644ab7a6f9c3f9 Mon Sep 17 00:00:00 2001 From: Yury Shkoda Date: Thu, 14 Oct 2021 07:32:16 +0000 Subject: [PATCH 08/12] feat(session): add SessionController --- src/controllers/Session.ts | 127 +++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 src/controllers/Session.ts diff --git a/src/controllers/Session.ts b/src/controllers/Session.ts new file mode 100644 index 0000000..3e9af19 --- /dev/null +++ b/src/controllers/Session.ts @@ -0,0 +1,127 @@ +import { Session } from '../types' +import { getTmpSessionsFolderPath, generateUniqueFileName } from '../utils' +import { + deleteFolder, + createFile, + fileExists, + deleteFile, + generateTimestamp +} from '@sasjs/utils' +import path from 'path' +import { ExecutionController } from './Execution' + +export class SessionController { + private sessions: Session[] = [] + private executionController: ExecutionController + + constructor() { + this.executionController = new ExecutionController() + } + + public async getSession() { + const readySessions = this.sessions.filter((sess: Session) => sess.ready) + + const session = readySessions.length + ? readySessions[0] + : await this.createSession() + + if (readySessions.length < 2) this.createSession() + + return session + } + + private async createSession() { + const sessionId = generateUniqueFileName(generateTimestamp()) + const sessionFolder = path.join(await getTmpSessionsFolderPath(), sessionId) + + const autoExecContent = `data _null_; + /* remove the dummy SYSIN */ + length fname $8; + rc=filename(fname,getoption('SYSIN') ); + if rc = 0 and fexist(fname) then rc=fdelete(fname); + rc=filename(fname); + /* now wait for the real SYSIN */ + slept=0; + do until ( fileexist(getoption('SYSIN')) or slept>(60*15) ); + slept=slept+sleep(0.01,1); + end; + run; + EOL` + + const autoExec = path.join(sessionFolder, 'autoexec.sas') + await createFile(autoExec, autoExecContent) + + await createFile(path.join(sessionFolder, 'code.sas'), '') + + const creationTimeStamp = sessionId.split('-').pop() as string + + const session: Session = { + id: sessionId, + ready: false, + creationTimeStamp: creationTimeStamp, + deathTimeStamp: ( + parseInt(creationTimeStamp) + + 15 * 60 * 1000 - + 1000 + ).toString(), + path: sessionFolder, + inUse: false + } + + this.scheduleSessionDestroy(session) + + this.executionController.execute('', autoExec, session).catch(() => {}) + + this.sessions.push(session) + + await this.waitForSession(session) + + return session + } + + public async waitForSession(session: Session) { + if (await fileExists(path.join(session.path, 'code.sas'))) { + while (await fileExists(path.join(session.path, 'code.sas'))) {} + + await deleteFile(path.join(session.path, 'log.log')) + + session.ready = true + + return Promise.resolve(session) + } else { + session.ready = true + + return Promise.resolve(session) + } + } + + public async deleteSession(session: Session) { + await deleteFolder(session.path) + + if (session.ready) { + this.sessions = this.sessions.filter( + (sess: Session) => sess.id !== session.id + ) + } + } + + private scheduleSessionDestroy(session: Session) { + setTimeout(async () => { + if (session.inUse) { + session.deathTimeStamp = session.deathTimeStamp + 1000 * 10 + + this.scheduleSessionDestroy(session) + } else { + await this.deleteSession(session) + } + }, parseInt(session.deathTimeStamp) - new Date().getTime() - 100) + } +} + +export const getSessionController = () => { + if (process.sessionController) return process.sessionController + + process.sessionController = new SessionController() + + return process.sessionController +} From 9cf02b25d03acee9df3052f5b4a4850395ad3823 Mon Sep 17 00:00:00 2001 From: Yury Shkoda Date: Thu, 14 Oct 2021 07:34:08 +0000 Subject: [PATCH 09/12] refactor: improve types and imports --- src/controllers/deploy.ts | 1 + src/controllers/executor.ts | 68 --------------- src/controllers/index.ts | 4 +- src/controllers/sas.ts | 107 ------------------------ src/controllers/session.ts | 109 ------------------------- src/types/{sas.ts => Execution.ts} | 0 src/types/{fileTree.ts => FileTree.ts} | 0 src/types/Process.d.ts | 2 +- src/types/{request.ts => Request.ts} | 1 + src/types/Session.ts | 3 +- src/types/index.ts | 6 +- 11 files changed, 10 insertions(+), 291 deletions(-) delete mode 100644 src/controllers/executor.ts delete mode 100644 src/controllers/sas.ts delete mode 100644 src/controllers/session.ts rename src/types/{sas.ts => Execution.ts} (100%) rename src/types/{fileTree.ts => FileTree.ts} (100%) rename src/types/{request.ts => Request.ts} (99%) diff --git a/src/controllers/deploy.ts b/src/controllers/deploy.ts index 3df7c43..2c1f1db 100644 --- a/src/controllers/deploy.ts +++ b/src/controllers/deploy.ts @@ -3,6 +3,7 @@ import { getTmpFilesFolderPath } from '../utils/file' import { createFolder, createFile, asyncForEach } from '@sasjs/utils' import path from 'path' +// REFACTOR: export FileTreeCpntroller export const createFileTree = async ( members: [FolderMember, ServiceMember], parentFolders: string[] = [] diff --git a/src/controllers/executor.ts b/src/controllers/executor.ts deleted file mode 100644 index db00297..0000000 --- a/src/controllers/executor.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { getSessionController } from './session' -import { readFile, deleteFile, fileExists, createFile } from '@sasjs/utils' -import path from 'path' -import { configuration } from '../../package.json' -import { promisify } from 'util' -import { execFile } from 'child_process' -const execFilePromise = promisify(execFile) - -export class ExecutionController { - async execute(program = '', autoExec?: string, debug?: number) { - console.log(`[ExecutionController]program: `, program) - console.log(`[ExecutionController]autoExec: `, autoExec) - - if (program) { - if (!(await fileExists(program))) { - throw 'SASjsServer/Executor: SAS file does not exist.' - } - - program = await readFile(program) - } - - const sessionController = getSessionController() - const session = await sessionController.getSession() - - console.log(`[ExecutionController]session: `, session) - - let log = path.join(session.path, 'log.log') - await createFile(log, '') - - let webout = path.join(session.path, 'webout.txt') - await createFile(webout, '') - - const code = path.join(session.path, 'code') - await createFile(code, program) - - let additionalArgs: string[] = [] - if (autoExec) additionalArgs = ['-AUTOEXEC', autoExec] - - const { stdout, stderr } = await execFilePromise(configuration.sasPath, [ - '-SYSIN', - code, - '-LOG', - log, - '-WORK', - ...additionalArgs, - session.path, - process.platform === 'win32' ? '-nosplash' : '' - ]).catch((err) => ({ stderr: err, stdout: '' })) - - log = await readFile(log) - - if (stderr) return Promise.reject({ error: stderr, log: log }) - - webout = await readFile(webout) - - if (debug && debug >= 131) { - webout = ` -${webout} -
-

SAS Log

-
${log}
-
-` - } - - return Promise.resolve(webout) - } -} diff --git a/src/controllers/index.ts b/src/controllers/index.ts index 4df137d..2990104 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,3 +1,3 @@ -export * from './sas' export * from './deploy' -export * from './session' +export * from './Session' +export * from './Execution' diff --git a/src/controllers/sas.ts b/src/controllers/sas.ts deleted file mode 100644 index e30e382..0000000 --- a/src/controllers/sas.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { readFile, deleteFile, fileExists, createFile } from '@sasjs/utils' -import path from 'path' -import { ExecutionResult, ExecutionQuery } from '../types' -import { - getTmpFilesFolderPath, - getTmpLogFolderPath, - getTmpWeboutFolderPath, - generateUniqueFileName -} from '../utils' -import { configuration } from '../../package.json' -import { SessionController } from './session' -import { promisify } from 'util' -import { execFile } from 'child_process' -const execFilePromise = promisify(execFile) - -export const processSas = async (query: ExecutionQuery): Promise => { - const sasCodePath = path - .join(getTmpFilesFolderPath(), query._program) - .replace(new RegExp('/', 'g'), path.sep) - - if (!(await fileExists(sasCodePath))) { - return Promise.reject({ error: 'SAS file does not exist.' }) - } - - // FIXME - const sessionController = new SessionController() - - sessionController.getSession() - - return Promise.resolve('success') - - // const sasFile: string = sasCodePath.split(path.sep).pop() || 'default' - - // const sasLogPath = path.join( - // getTmpLogFolderPath(), - // generateUniqueFileName(sasFile.replace(/\.sas/g, ''), '.log') - // ) - - // await createFile(sasLogPath, '') - - // const sasWeboutPath = path.join( - // getTmpWeboutFolderPath(), - // generateUniqueFileName(sasFile.replace(/\.sas/g, ''), '.txt') - // ) - - // await createFile(sasWeboutPath, '') - - // let sasCode = await readFile(sasCodePath) - - // const vars: any = query - // Object.keys(query).forEach( - // (key: string) => (sasCode = `%let ${key}=${vars[key]};\n${sasCode}`) - // ) - - // sasCode = `filename _webout "${sasWeboutPath}";\n${sasCode}` - - // const tmpSasCodePath = sasCodePath.replace( - // sasFile, - // generateUniqueFileName(sasFile) - // ) - - // await createFile(tmpSasCodePath, sasCode) - - // const { stdout, stderr } = await execFilePromise(configuration.sasPath, [ - // '-SYSIN', - // tmpSasCodePath, - // '-log', - // sasLogPath, - // process.platform === 'win32' ? '-nosplash' : '' - // ]).catch((err) => ({ stderr: err, stdout: '' })) - - // let log = '' - // if (sasLogPath && (await fileExists(sasLogPath))) { - // log = await readFile(sasLogPath) - // } - - // await deleteFile(sasLogPath) - // await deleteFile(tmpSasCodePath) - - // if (stderr) return Promise.reject({ error: stderr, log: log }) - - // if (await fileExists(sasWeboutPath)) { - // let webout = await readFile(sasWeboutPath) - - // await deleteFile(sasWeboutPath) - - // const debug = Object.keys(query).find( - // (key: string) => key.toLowerCase() === '_debug' - // ) - - // if (debug && (query as any)[debug] >= 131) { - // webout = ` - // ${webout} - //
- //

SAS Log

- //
${log}
- //
- // ` - // } - - // return Promise.resolve(webout) - // } else { - // return Promise.resolve({ - // log: log - // }) - // } -} diff --git a/src/controllers/session.ts b/src/controllers/session.ts deleted file mode 100644 index 0b17c73..0000000 --- a/src/controllers/session.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { Session } from '../types' -import { getTmpSessionsFolderPath, generateUniqueFileName } from '../utils' -import { createFolder, createFile, generateTimestamp } from '@sasjs/utils' -import path from 'path' -import { ExecutionController } from './executor' - -// /opt/sas/sas9/SASHome/SASFoundation/9.4/sasexe/sas -// -LOG /tmp/mydir/demo.log -// -SYSIN /tmp/mydir/code.sas -// -AUTOEXEC /tmp/mydir/autoexec.sas -// -WORK /tmp/mydir - -// 1. req (_program) for execution -// 2. check available session -// 2.3 spawn one more session -// 2.3.1 create folder -// 2.3.2 create autoexec -// 2.3.3 create _program.sas (empty) -// 2.3.4 /opt/sas/sas9/SASHome/SASFoundation/9.4/sasexe/sas -LOG /tmp/sessionFolder/demo.log -SYSIN /tmp/sessionFolder/_program.sas -AUTOEXEC /tmp/sessionFolder/autoexec.sas -WORK /tmp/sessionFolder -// 2.3.5 wait for _program.sas to be deleted -// 2.3.6 add session to the session array -// --- -// 3.0 wait for session -// 3.1 create _program.sas in sessionFolder -// 3.2 poll session array - -export class SessionController { - private sessions: Session[] = [] - private executionController: ExecutionController - - constructor() { - this.executionController = new ExecutionController() - } - - public async getSession() { - if (this.sessions.length) { - const session: Session = this.sessions[0] - - // TODO: check if session is not expired - - return session - } - - return await this.createSession() - } - - private async createSession() { - if (!this.sessions.length) { - const sessionId = generateUniqueFileName(generateTimestamp()) - - const sessionFolder = path.join( - await getTmpSessionsFolderPath(), - sessionId - ) - - const autoExecContent = `data _null_; - /* remove the dummy SYSIN */ - length fname $8; - rc=filename(fname,getoption('SYSIN') ); - if rc = 0 and fexist(fname) then rc=fdelete(fname); - rc=filename(fname); - /* now wait for the real SYSIN */ - slept=0; - do until ( fileexist(getoption('SYSIN')) or slept>(60*15) ); - slept=slept+sleep(0.1,1); - end; -run; -EOL` - - const autoExec = path.join(sessionFolder, 'autoexec.sas') - - await createFile(autoExec, autoExecContent) - - console.log(`[SessionController about to create first session]`) - - this.executionController.execute('', autoExec) - - const creationTimeStamp = sessionId.split('-').pop() as string - - const session: Session = { - id: sessionId, - available: true, - creationTimeStamp: creationTimeStamp, - deathTimeStamp: ( - parseInt(creationTimeStamp) + - 15 * 60 * 1000 - - 1000 - ).toString(), - path: sessionFolder - } - - console.log(`[SessionController]session: `, session) - - this.sessions.push(session) - - return session - } else { - return this.sessions[0] - } - } -} - -export const getSessionController = () => { - if (process.sessionController) return process.sessionController - - process.sessionController = new SessionController() - - return process.sessionController -} diff --git a/src/types/sas.ts b/src/types/Execution.ts similarity index 100% rename from src/types/sas.ts rename to src/types/Execution.ts diff --git a/src/types/fileTree.ts b/src/types/FileTree.ts similarity index 100% rename from src/types/fileTree.ts rename to src/types/FileTree.ts diff --git a/src/types/Process.d.ts b/src/types/Process.d.ts index 501fd16..50313f7 100644 --- a/src/types/Process.d.ts +++ b/src/types/Process.d.ts @@ -1,5 +1,5 @@ declare namespace NodeJS { export interface Process { - sessionController?: import('../controllers/session').SessionController + sessionController?: import('../controllers/Session').SessionController } } diff --git a/src/types/request.ts b/src/types/Request.ts similarity index 99% rename from src/types/request.ts rename to src/types/Request.ts index 26c872f..6bc7e2b 100644 --- a/src/types/request.ts +++ b/src/types/Request.ts @@ -1,4 +1,5 @@ import { MacroVars } from '@sasjs/utils' + export interface ExecutionQuery { _program: string macroVars?: MacroVars diff --git a/src/types/Session.ts b/src/types/Session.ts index 9066363..5484311 100644 --- a/src/types/Session.ts +++ b/src/types/Session.ts @@ -1,7 +1,8 @@ export interface Session { id: string - available: boolean + ready: boolean creationTimeStamp: string deathTimeStamp: string path: string + inUse: boolean } diff --git a/src/types/index.ts b/src/types/index.ts index 1c326b2..2377b59 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,5 +1,5 @@ // TODO: uppercase types -export * from './sas' -export * from './request' -export * from './fileTree' +export * from './Execution' +export * from './Request' +export * from './FileTree' export * from './Session' From 129cb7c128155e72d83341589c23af92fdfad59c Mon Sep 17 00:00:00 2001 From: Yury Shkoda Date: Thu, 14 Oct 2021 07:34:37 +0000 Subject: [PATCH 10/12] chore(utils): add sleep util --- src/routes/index.ts | 38 +++++++++++++++----------------------- src/utils/sleep.ts | 3 +++ 2 files changed, 18 insertions(+), 23 deletions(-) create mode 100644 src/utils/sleep.ts diff --git a/src/routes/index.ts b/src/routes/index.ts index bd27d0a..1fc3bc9 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,23 +1,14 @@ import express from 'express' -import { processSas, createFileTree, getTreeExample } from '../controllers' +import { createFileTree, getTreeExample } from '../controllers' import { ExecutionResult, isRequestQuery, isFileTree } from '../types' +import path from 'path' +import { getTmpFilesFolderPath } from '../utils' +import { ExecutionController } from '../controllers' const router = express.Router() -router.get('/', async (req, res) => { - const query = req.query - - if (!isRequestQuery(query)) { - res.send('Welcome to @sasjs/server API') - - return - } - - const result: ExecutionResult = await processSas(query) - - res.send(`Executed!
-

Log is located:

${result.logPath}
-

Log:

`) +router.get('/', async (_, res) => { + res.status(200).send('Welcome to @sasjs/server API') }) router.post('/deploy', async (req, res) => { @@ -54,20 +45,21 @@ router.get('/SASjsExecutor', async (req, res) => { }) router.get('/SASjsExecutor/do', async (req, res) => { - const queryEntries = Object.keys(req.query).map((entry: string) => - entry.toLowerCase() - ) - if (isRequestQuery(req.query)) { - await processSas({ ...req.query }) - .then((result) => { + const sasCodePath = path + .join(getTmpFilesFolderPath(), req.query._program) + .replace(new RegExp('/', 'g'), path.sep) + + await new ExecutionController() + .execute(sasCodePath, undefined, undefined, { ...req.query }) + .then((result: {}) => { res.status(200).send(result) }) - .catch((err) => { + .catch((err: {} | string) => { res.status(400).send({ status: 'failure', message: 'Job execution failed.', - ...err + ...(typeof err === 'object' ? err : { details: err }) }) }) } else { diff --git a/src/utils/sleep.ts b/src/utils/sleep.ts new file mode 100644 index 0000000..61d7522 --- /dev/null +++ b/src/utils/sleep.ts @@ -0,0 +1,3 @@ +export const sleep = async (delay: number) => { + await new Promise((resolve) => setTimeout(resolve, delay)) +} From ba0722b98c41ab7452a56c468090f90260028fc3 Mon Sep 17 00:00:00 2001 From: Yury Shkoda Date: Thu, 14 Oct 2021 07:35:10 +0000 Subject: [PATCH 11/12] chore(utils): add sleep export --- src/utils/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/index.ts b/src/utils/index.ts index d0a70d5..31581eb 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1 +1,2 @@ export * from './file' +export * from './sleep' From 00c7d2150c87639764d3d2658642e2221a8309c7 Mon Sep 17 00:00:00 2001 From: Yury Shkoda Date: Thu, 14 Oct 2021 07:40:01 +0000 Subject: [PATCH 12/12] chore(git): add blank line to the file end --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index fb07265..9a770f0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,4 +2,4 @@ "cSpell.words": [ "autoexec" ] -} \ No newline at end of file +}