mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e67d27d264 | ||
|
|
53033ccc96 | ||
|
|
6131ed1cbe | ||
|
|
5d624e3399 | ||
| ee17d37aa1 | |||
|
|
476f834a80 | ||
|
|
8b8739a873 | ||
| bce83cb6fb | |||
| 3a3c90d9e6 | |||
|
|
e63eaa5302 | ||
|
|
65de1bb175 | ||
|
|
a5ee2f2923 | ||
| 98ea2ac9b9 | |||
|
|
e94c56b23f | ||
|
|
64f80e958d | ||
| bd97363c13 | |||
| 02e88ae728 | |||
| 882bedd5d5 | |||
| 8780b800a3 | |||
| 4c11082796 | |||
| a9b25b8880 | |||
| b06993ab9e | |||
|
|
f736e67517 | ||
|
|
0f4a60c0c7 | ||
|
|
f8bb7327a8 |
54
CHANGELOG.md
54
CHANGELOG.md
@@ -1,3 +1,57 @@
|
|||||||
|
## [0.17.2](https://github.com/sasjs/server/compare/v0.17.1...v0.17.2) (2022-08-31)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* addition of SASINITIALFOLDER startup option. Closes [#260](https://github.com/sasjs/server/issues/260) ([a5ee2f2](https://github.com/sasjs/server/commit/a5ee2f292384f90e9d95d003d652311c0d91a7a7))
|
||||||
|
|
||||||
|
## [0.17.1](https://github.com/sasjs/server/compare/v0.17.0...v0.17.1) (2022-08-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* typo mistake ([ee17d37](https://github.com/sasjs/server/commit/ee17d37aa188b0ca43cea0e89d6cd1a566b765cb))
|
||||||
|
|
||||||
|
# [0.17.0](https://github.com/sasjs/server/compare/v0.16.1...v0.17.0) (2022-08-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* allow underscores in file name ([bce83cb](https://github.com/sasjs/server/commit/bce83cb6fbc98f8198564c9399821f5829acc767))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add the functionality of saving file by ctrl + s in editor ([3a3c90d](https://github.com/sasjs/server/commit/3a3c90d9e690ac5267bf1acc834b5b5c5b4dadb6))
|
||||||
|
|
||||||
|
## [0.16.1](https://github.com/sasjs/server/compare/v0.16.0...v0.16.1) (2022-08-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* update response of /SASjsApi/stp/execute and /SASjsApi/code/execute ([98ea2ac](https://github.com/sasjs/server/commit/98ea2ac9b98631605e39e5900e533727ea0e3d85))
|
||||||
|
|
||||||
|
# [0.16.0](https://github.com/sasjs/server/compare/v0.15.3...v0.16.0) (2022-08-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add a new variable _SASJS_WEBOUT_HEADERS to code.js and code.py ([882bedd](https://github.com/sasjs/server/commit/882bedd5d5da22de6ed45c03d0a261aadfb3a33c))
|
||||||
|
* update content for code.sas file ([02e88ae](https://github.com/sasjs/server/commit/02e88ae7280d020a753bc2c095a931c79ac392d1))
|
||||||
|
* update default content type for python and js runtimes ([8780b80](https://github.com/sasjs/server/commit/8780b800a34aa618631821e5d97e26e8b0f15806))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* implement the logic for running python stored programs ([b06993a](https://github.com/sasjs/server/commit/b06993ab9ea24b28d9e553763187387685aaa666))
|
||||||
|
|
||||||
|
## [0.15.3](https://github.com/sasjs/server/compare/v0.15.2...v0.15.3) (2022-08-11)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* adding proc printto in precode to enable print output in log. Closes [#253](https://github.com/sasjs/server/issues/253) ([f8bb732](https://github.com/sasjs/server/commit/f8bb7327a8a4649ac77bb6237e31cea075d46bb9))
|
||||||
|
|
||||||
## [0.15.2](https://github.com/sasjs/server/compare/v0.15.1...v0.15.2) (2022-08-10)
|
## [0.15.2](https://github.com/sasjs/server/compare/v0.15.1...v0.15.2) (2022-08-10)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
22
README.md
22
README.md
@@ -64,12 +64,27 @@ Example contents of a `.env` file:
|
|||||||
# Server mode is multi-user and suitable for intranet / internet use
|
# Server mode is multi-user and suitable for intranet / internet use
|
||||||
MODE=
|
MODE=
|
||||||
|
|
||||||
|
# A comma separated string that defines the available runTimes.
|
||||||
|
# Priority is given to the runtime that comes first in the string.
|
||||||
|
# Possible options at the moment are sas and js
|
||||||
|
|
||||||
|
# This string sets the priority of the available analytic runtimes
|
||||||
|
# Valid runtimes are SAS (sas), JavaScript (js) and Python (py)
|
||||||
|
# For each option provided, there should be a corresponding path,
|
||||||
|
# eg SAS_PATH, NODE_PATH or PYTHON_PATH
|
||||||
|
# Priority is given to runtimes earlier in the string
|
||||||
|
# Example options: [sas,js,py | js,py | sas | sas,js]
|
||||||
|
RUN_TIMES=
|
||||||
|
|
||||||
# Path to SAS executable (sas.exe / sas.sh)
|
# Path to SAS executable (sas.exe / sas.sh)
|
||||||
SAS_PATH=/path/to/sas/executable.exe
|
SAS_PATH=/path/to/sas/executable.exe
|
||||||
|
|
||||||
# Path to Node.js executable
|
# Path to Node.js executable
|
||||||
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
|
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
|
||||||
|
|
||||||
|
# Path to Python executable
|
||||||
|
PYTHON_PATH=/usr/bin/python
|
||||||
|
|
||||||
# Path to working directory
|
# Path to working directory
|
||||||
# This location is for SAS WORK, staged files, DRIVE, configuration etc
|
# This location is for SAS WORK, staged files, DRIVE, configuration etc
|
||||||
SASJS_ROOT=./sasjs_root
|
SASJS_ROOT=./sasjs_root
|
||||||
@@ -139,13 +154,6 @@ LOG_FORMAT_MORGAN=
|
|||||||
# This location is for server logs with classical UNIX logrotate behavior
|
# This location is for server logs with classical UNIX logrotate behavior
|
||||||
LOG_LOCATION=./sasjs_root/logs
|
LOG_LOCATION=./sasjs_root/logs
|
||||||
|
|
||||||
# A comma separated string that defines the available runTimes.
|
|
||||||
# Priority is given to the runtime that comes first in the string.
|
|
||||||
# Possible options at the moment are sas and js
|
|
||||||
|
|
||||||
# options: [sas,js|js,sas|sas|js] default:sas
|
|
||||||
RUN_TIMES=
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Persisting the Session
|
## Persisting the Session
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ HELMET_COEP=[true|false] if omitted HELMET default will be used
|
|||||||
|
|
||||||
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
|
||||||
|
|
||||||
RUN_TIMES=[sas|js|sas,js|js,sas] default considered as sas
|
RUN_TIMES=[sas,js,py | js,py | sas | sas,js] default considered as sas
|
||||||
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
|
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
|
||||||
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
|
NODE_PATH=~/.nvm/versions/node/v16.14.0/bin/node
|
||||||
|
PYTHON_PATH=/usr/bin/python
|
||||||
|
|
||||||
SASJS_ROOT=./sasjs_root
|
SASJS_ROOT=./sasjs_root
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
|
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
|
||||||
import { ExecuteReturnJson, ExecutionController } from './internal'
|
import { ExecutionController } from './internal'
|
||||||
import { ExecuteReturnJsonResponse } from '.'
|
|
||||||
import {
|
import {
|
||||||
getPreProgramVariables,
|
getPreProgramVariables,
|
||||||
getUserAutoExec,
|
getUserAutoExec,
|
||||||
@@ -35,7 +34,7 @@ export class CodeController {
|
|||||||
public async executeCode(
|
public async executeCode(
|
||||||
@Request() request: express.Request,
|
@Request() request: express.Request,
|
||||||
@Body() body: ExecuteCodePayload
|
@Body() body: ExecuteCodePayload
|
||||||
): Promise<ExecuteReturnJsonResponse> {
|
): Promise<string | Buffer> {
|
||||||
return executeCode(request, body)
|
return executeCode(request, body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,22 +50,15 @@ const executeCode = async (
|
|||||||
: await getUserAutoExec()
|
: await getUserAutoExec()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { webout, log, httpHeaders } =
|
const { result } = await new ExecutionController().executeProgram({
|
||||||
(await new ExecutionController().executeProgram({
|
program: code,
|
||||||
program: code,
|
preProgramVariables: getPreProgramVariables(req),
|
||||||
preProgramVariables: getPreProgramVariables(req),
|
vars: { ...req.query, _debug: 131 },
|
||||||
vars: { ...req.query, _debug: 131 },
|
otherArgs: { userAutoExec },
|
||||||
otherArgs: { userAutoExec },
|
runTime: runTime
|
||||||
returnJson: true,
|
})
|
||||||
runTime: runTime
|
|
||||||
})) as ExecuteReturnJson
|
|
||||||
|
|
||||||
return {
|
return result
|
||||||
status: 'success',
|
|
||||||
_webout: webout as string,
|
|
||||||
log: parseLogToArray(log),
|
|
||||||
httpHeaders
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
throw {
|
throw {
|
||||||
code: 400,
|
code: 400,
|
||||||
|
|||||||
@@ -20,12 +20,6 @@ export interface ExecuteReturnRaw {
|
|||||||
result: string | Buffer
|
result: string | Buffer
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExecuteReturnJson {
|
|
||||||
httpHeaders: HTTPHeaders
|
|
||||||
webout: string | Buffer
|
|
||||||
log?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ExecuteFileParams {
|
interface ExecuteFileParams {
|
||||||
programPath: string
|
programPath: string
|
||||||
preProgramVariables: PreProgramVars
|
preProgramVariables: PreProgramVars
|
||||||
@@ -68,10 +62,9 @@ export class ExecutionController {
|
|||||||
preProgramVariables,
|
preProgramVariables,
|
||||||
vars,
|
vars,
|
||||||
otherArgs,
|
otherArgs,
|
||||||
returnJson,
|
|
||||||
session: sessionByFileUpload,
|
session: sessionByFileUpload,
|
||||||
runTime
|
runTime
|
||||||
}: ExecuteProgramParams): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
|
}: ExecuteProgramParams): Promise<ExecuteReturnRaw> {
|
||||||
const sessionController = getSessionController(runTime)
|
const sessionController = getSessionController(runTime)
|
||||||
|
|
||||||
const session =
|
const session =
|
||||||
@@ -96,6 +89,7 @@ export class ExecutionController {
|
|||||||
vars,
|
vars,
|
||||||
session,
|
session,
|
||||||
weboutPath,
|
weboutPath,
|
||||||
|
headersPath,
|
||||||
tokenFile,
|
tokenFile,
|
||||||
runTime,
|
runTime,
|
||||||
logPath,
|
logPath,
|
||||||
@@ -107,10 +101,7 @@ export class ExecutionController {
|
|||||||
? await readFile(headersPath)
|
? await readFile(headersPath)
|
||||||
: ''
|
: ''
|
||||||
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
|
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
|
||||||
const fileResponse: boolean =
|
const fileResponse: boolean = httpHeaders.hasOwnProperty('content-type')
|
||||||
httpHeaders.hasOwnProperty('content-type') &&
|
|
||||||
!returnJson && // not a POST Request
|
|
||||||
!isDebugOn(vars) // Debug is not enabled
|
|
||||||
|
|
||||||
const webout = (await fileExists(weboutPath))
|
const webout = (await fileExists(weboutPath))
|
||||||
? fileResponse
|
? fileResponse
|
||||||
@@ -121,19 +112,11 @@ export class ExecutionController {
|
|||||||
// it should be deleted by scheduleSessionDestroy
|
// it should be deleted by scheduleSessionDestroy
|
||||||
session.inUse = false
|
session.inUse = false
|
||||||
|
|
||||||
if (returnJson) {
|
|
||||||
return {
|
|
||||||
httpHeaders,
|
|
||||||
webout,
|
|
||||||
log: isDebugOn(vars) || session.crashed ? log : undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
httpHeaders,
|
httpHeaders,
|
||||||
result:
|
result:
|
||||||
isDebugOn(vars) || session.crashed
|
isDebugOn(vars) || session.crashed
|
||||||
? `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
|
? `${webout}\n${process.logsUUID}\n${log}`
|
||||||
: webout
|
: webout
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,6 +101,8 @@ ${autoExecContent}`
|
|||||||
session.path,
|
session.path,
|
||||||
'-AUTOEXEC',
|
'-AUTOEXEC',
|
||||||
autoExecPath,
|
autoExecPath,
|
||||||
|
'-SASINITIALFOLDER',
|
||||||
|
session.path,
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-nosplash' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-nosplash' : '',
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-icon' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-icon' : '',
|
||||||
process.sasLoc!.endsWith('sas.exe') ? '-nodms' : '',
|
process.sasLoc!.endsWith('sas.exe') ? '-nodms' : '',
|
||||||
@@ -190,7 +192,39 @@ export class JSSessionController extends SessionController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
||||||
await createFile(headersPath, 'Content-type: application/json')
|
await createFile(headersPath, 'Content-type: text/plain')
|
||||||
|
|
||||||
|
this.sessions.push(session)
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PythonSessionController extends SessionController {
|
||||||
|
protected async createSession(): Promise<Session> {
|
||||||
|
const sessionId = generateUniqueFileName(generateTimestamp())
|
||||||
|
const sessionFolder = path.join(getSessionsFolder(), sessionId)
|
||||||
|
|
||||||
|
const creationTimeStamp = sessionId.split('-').pop() as string
|
||||||
|
// death time of session is 15 mins from creation
|
||||||
|
const deathTimeStamp = (
|
||||||
|
parseInt(creationTimeStamp) +
|
||||||
|
15 * 60 * 1000 -
|
||||||
|
1000
|
||||||
|
).toString()
|
||||||
|
|
||||||
|
const session: Session = {
|
||||||
|
id: sessionId,
|
||||||
|
ready: true,
|
||||||
|
inUse: true,
|
||||||
|
consumed: false,
|
||||||
|
completed: false,
|
||||||
|
creationTimeStamp,
|
||||||
|
deathTimeStamp,
|
||||||
|
path: sessionFolder
|
||||||
|
}
|
||||||
|
|
||||||
|
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
||||||
|
await createFile(headersPath, 'Content-type: text/plain')
|
||||||
|
|
||||||
this.sessions.push(session)
|
this.sessions.push(session)
|
||||||
return session
|
return session
|
||||||
@@ -199,7 +233,7 @@ export class JSSessionController extends SessionController {
|
|||||||
|
|
||||||
export const getSessionController = (
|
export const getSessionController = (
|
||||||
runTime: RunTimeType
|
runTime: RunTimeType
|
||||||
): SASSessionController | JSSessionController => {
|
): SASSessionController | JSSessionController | PythonSessionController => {
|
||||||
if (runTime === RunTimeType.SAS) {
|
if (runTime === RunTimeType.SAS) {
|
||||||
return getSASSessionController()
|
return getSASSessionController()
|
||||||
}
|
}
|
||||||
@@ -208,6 +242,10 @@ export const getSessionController = (
|
|||||||
return getJSSessionController()
|
return getJSSessionController()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (runTime === RunTimeType.PY) {
|
||||||
|
return getPythonSessionController()
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error('No Runtime is configured')
|
throw new Error('No Runtime is configured')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,6 +265,14 @@ const getJSSessionController = (): JSSessionController => {
|
|||||||
return process.jsSessionController
|
return process.jsSessionController
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getPythonSessionController = (): PythonSessionController => {
|
||||||
|
if (process.pythonSessionController) return process.pythonSessionController
|
||||||
|
|
||||||
|
process.pythonSessionController = new PythonSessionController()
|
||||||
|
|
||||||
|
return process.pythonSessionController
|
||||||
|
}
|
||||||
|
|
||||||
const autoExecContent = `
|
const autoExecContent = `
|
||||||
data _null_;
|
data _null_;
|
||||||
/* remove the dummy SYSIN */
|
/* remove the dummy SYSIN */
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const createJSProgram = async (
|
|||||||
vars: ExecutionVars,
|
vars: ExecutionVars,
|
||||||
session: Session,
|
session: Session,
|
||||||
weboutPath: string,
|
weboutPath: string,
|
||||||
|
headersPath: string,
|
||||||
tokenFile: string,
|
tokenFile: string,
|
||||||
otherArgs?: any
|
otherArgs?: any
|
||||||
) => {
|
) => {
|
||||||
@@ -23,15 +24,16 @@ let _webout = '';
|
|||||||
const weboutPath = '${
|
const weboutPath = '${
|
||||||
isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath
|
isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath
|
||||||
}';
|
}';
|
||||||
const _sasjs_tokenfile = '${
|
const _SASJS_TOKENFILE = '${
|
||||||
isWindows() ? tokenFile.replace(/\\/g, '\\\\') : tokenFile
|
isWindows() ? tokenFile.replace(/\\/g, '\\\\') : tokenFile
|
||||||
}';
|
}';
|
||||||
const _sasjs_username = '${preProgramVariables?.username}';
|
const _SASJS_WEBOUT_HEADERS = '${headersPath}';
|
||||||
const _sasjs_userid = '${preProgramVariables?.userId}';
|
const _SASJS_USERNAME = '${preProgramVariables?.username}';
|
||||||
const _sasjs_displayname = '${preProgramVariables?.displayName}';
|
const _SASJS_USERID = '${preProgramVariables?.userId}';
|
||||||
const _metaperson = _sasjs_displayname;
|
const _SASJS_DISPLAYNAME = '${preProgramVariables?.displayName}';
|
||||||
const _metauser = _sasjs_username;
|
const _METAPERSON = _SASJS_DISPLAYNAME;
|
||||||
const sasjsprocessmode = 'Stored Program';
|
const _METAUSER = _SASJS_USERNAME;
|
||||||
|
const SASJSPROCESSMODE = 'Stored Program';
|
||||||
`
|
`
|
||||||
|
|
||||||
const requiredModules = `const fs = require('fs')`
|
const requiredModules = `const fs = require('fs')`
|
||||||
@@ -55,14 +57,15 @@ if (_webout) {
|
|||||||
`
|
`
|
||||||
// if no files are uploaded filesNamesMap will be undefined
|
// if no files are uploaded filesNamesMap will be undefined
|
||||||
if (otherArgs?.filesNamesMap) {
|
if (otherArgs?.filesNamesMap) {
|
||||||
const uploadJSCode = await generateFileUploadJSCode(
|
const uploadJsCode = await generateFileUploadJSCode(
|
||||||
otherArgs.filesNamesMap,
|
otherArgs.filesNamesMap,
|
||||||
session.path
|
session.path
|
||||||
)
|
)
|
||||||
|
|
||||||
//If js code for the file is generated it will be appended to the top of jsCode
|
// If any files are uploaded, the program needs to be updated with some
|
||||||
if (uploadJSCode.length > 0) {
|
// dynamically generated variables (pointers) for ease of ingestion
|
||||||
program = `${uploadJSCode}\n` + program
|
if (uploadJsCode.length > 0) {
|
||||||
|
program = `${uploadJsCode}\n` + program
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return requiredModules + program
|
return requiredModules + program
|
||||||
|
|||||||
68
api/src/controllers/internal/createPythonProgram.ts
Normal file
68
api/src/controllers/internal/createPythonProgram.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { isWindows } from '@sasjs/utils'
|
||||||
|
import { PreProgramVars, Session } from '../../types'
|
||||||
|
import { generateFileUploadPythonCode } from '../../utils'
|
||||||
|
import { ExecutionVars } from './'
|
||||||
|
|
||||||
|
export const createPythonProgram = async (
|
||||||
|
program: string,
|
||||||
|
preProgramVariables: PreProgramVars,
|
||||||
|
vars: ExecutionVars,
|
||||||
|
session: Session,
|
||||||
|
weboutPath: string,
|
||||||
|
headersPath: string,
|
||||||
|
tokenFile: string,
|
||||||
|
otherArgs?: any
|
||||||
|
) => {
|
||||||
|
const varStatments = Object.keys(vars).reduce(
|
||||||
|
(computed: string, key: string) => `${computed}${key} = '${vars[key]}';\n`,
|
||||||
|
''
|
||||||
|
)
|
||||||
|
|
||||||
|
const preProgramVarStatments = `
|
||||||
|
_SASJS_SESSION_PATH = '${
|
||||||
|
isWindows() ? session.path.replace(/\\/g, '\\\\') : session.path
|
||||||
|
}';
|
||||||
|
_WEBOUT = '${isWindows() ? weboutPath.replace(/\\/g, '\\\\') : weboutPath}';
|
||||||
|
_SASJS_WEBOUT_HEADERS = '${headersPath}';
|
||||||
|
_SASJS_TOKENFILE = '${
|
||||||
|
isWindows() ? tokenFile.replace(/\\/g, '\\\\') : tokenFile
|
||||||
|
}';
|
||||||
|
_SASJS_USERNAME = '${preProgramVariables?.username}';
|
||||||
|
_SASJS_USERID = '${preProgramVariables?.userId}';
|
||||||
|
_SASJS_DISPLAYNAME = '${preProgramVariables?.displayName}';
|
||||||
|
_METAPERSON = _SASJS_DISPLAYNAME;
|
||||||
|
_METAUSER = _SASJS_USERNAME;
|
||||||
|
SASJSPROCESSMODE = 'Stored Program';
|
||||||
|
`
|
||||||
|
|
||||||
|
const requiredModules = `import os`
|
||||||
|
|
||||||
|
program = `
|
||||||
|
# runtime vars
|
||||||
|
${varStatments}
|
||||||
|
|
||||||
|
# dynamic user-provided vars
|
||||||
|
${preProgramVarStatments}
|
||||||
|
|
||||||
|
# change working directory to session folder
|
||||||
|
os.chdir(_SASJS_SESSION_PATH)
|
||||||
|
|
||||||
|
# actual job code
|
||||||
|
${program}
|
||||||
|
|
||||||
|
`
|
||||||
|
// if no files are uploaded filesNamesMap will be undefined
|
||||||
|
if (otherArgs?.filesNamesMap) {
|
||||||
|
const uploadPythonCode = await generateFileUploadPythonCode(
|
||||||
|
otherArgs.filesNamesMap,
|
||||||
|
session.path
|
||||||
|
)
|
||||||
|
|
||||||
|
// If any files are uploaded, the program needs to be updated with some
|
||||||
|
// dynamically generated variables (pointers) for ease of ingestion
|
||||||
|
if (uploadPythonCode.length > 0) {
|
||||||
|
program = `${uploadPythonCode}\n` + program
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return requiredModules + program
|
||||||
|
}
|
||||||
@@ -23,10 +23,14 @@ export const createSASProgram = async (
|
|||||||
%let _sasjs_displayname=${preProgramVariables?.displayName};
|
%let _sasjs_displayname=${preProgramVariables?.displayName};
|
||||||
%let _sasjs_apiserverurl=${preProgramVariables?.serverUrl};
|
%let _sasjs_apiserverurl=${preProgramVariables?.serverUrl};
|
||||||
%let _sasjs_apipath=/SASjsApi/stp/execute;
|
%let _sasjs_apipath=/SASjsApi/stp/execute;
|
||||||
|
%let _sasjs_webout_headers=%sysfunc(pathname(work))/../stpsrv_header.txt;
|
||||||
%let _metaperson=&_sasjs_displayname;
|
%let _metaperson=&_sasjs_displayname;
|
||||||
%let _metauser=&_sasjs_username;
|
%let _metauser=&_sasjs_username;
|
||||||
|
|
||||||
|
/* the below is here for compatibility and will be removed in a future release */
|
||||||
|
%let sasjs_stpsrv_header_loc=&_sasjs_webout_headers;
|
||||||
|
|
||||||
%let sasjsprocessmode=Stored Program;
|
%let sasjsprocessmode=Stored Program;
|
||||||
%let sasjs_stpsrv_header_loc=%sysfunc(pathname(work))/../stpsrv_header.txt;
|
|
||||||
|
|
||||||
%global SYSPROCESSMODE SYSTCPIPHOSTNAME SYSHOSTINFOLONG;
|
%global SYSPROCESSMODE SYSTCPIPHOSTNAME SYSHOSTINFOLONG;
|
||||||
%macro _sasjs_server_init();
|
%macro _sasjs_server_init();
|
||||||
@@ -34,6 +38,9 @@ export const createSASProgram = async (
|
|||||||
%if "&SYSTCPIPHOSTNAME"="" %then %let SYSTCPIPHOSTNAME=&_sasjs_apiserverurl;
|
%if "&SYSTCPIPHOSTNAME"="" %then %let SYSTCPIPHOSTNAME=&_sasjs_apiserverurl;
|
||||||
%mend;
|
%mend;
|
||||||
%_sasjs_server_init()
|
%_sasjs_server_init()
|
||||||
|
|
||||||
|
proc printto print="%sysfunc(getoption(log))";
|
||||||
|
run;
|
||||||
`
|
`
|
||||||
|
|
||||||
program = `
|
program = `
|
||||||
@@ -60,7 +67,8 @@ ${program}`
|
|||||||
session.path
|
session.path
|
||||||
)
|
)
|
||||||
|
|
||||||
//If sas code for the file is generated it will be appended to the top of sasCode
|
// If any files are uploaded, the program needs to be updated with some
|
||||||
|
// dynamically generated variables (pointers) for ease of ingestion
|
||||||
if (uploadSasCode.length > 0) {
|
if (uploadSasCode.length > 0) {
|
||||||
program = `${uploadSasCode}` + program
|
program = `${uploadSasCode}` + program
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ export * from './Execution'
|
|||||||
export * from './FileUploadController'
|
export * from './FileUploadController'
|
||||||
export * from './createSASProgram'
|
export * from './createSASProgram'
|
||||||
export * from './createJSProgram'
|
export * from './createJSProgram'
|
||||||
|
export * from './createPythonProgram'
|
||||||
export * from './processProgram'
|
export * from './processProgram'
|
||||||
|
|||||||
@@ -5,7 +5,12 @@ import { once } from 'stream'
|
|||||||
import { createFile, moveFile } from '@sasjs/utils'
|
import { createFile, moveFile } from '@sasjs/utils'
|
||||||
import { PreProgramVars, Session } from '../../types'
|
import { PreProgramVars, Session } from '../../types'
|
||||||
import { RunTimeType } from '../../utils'
|
import { RunTimeType } from '../../utils'
|
||||||
import { ExecutionVars, createSASProgram, createJSProgram } from './'
|
import {
|
||||||
|
ExecutionVars,
|
||||||
|
createSASProgram,
|
||||||
|
createJSProgram,
|
||||||
|
createPythonProgram
|
||||||
|
} from './'
|
||||||
|
|
||||||
export const processProgram = async (
|
export const processProgram = async (
|
||||||
program: string,
|
program: string,
|
||||||
@@ -13,6 +18,7 @@ export const processProgram = async (
|
|||||||
vars: ExecutionVars,
|
vars: ExecutionVars,
|
||||||
session: Session,
|
session: Session,
|
||||||
weboutPath: string,
|
weboutPath: string,
|
||||||
|
headersPath: string,
|
||||||
tokenFile: string,
|
tokenFile: string,
|
||||||
runTime: RunTimeType,
|
runTime: RunTimeType,
|
||||||
logPath: string,
|
logPath: string,
|
||||||
@@ -25,6 +31,7 @@ export const processProgram = async (
|
|||||||
vars,
|
vars,
|
||||||
session,
|
session,
|
||||||
weboutPath,
|
weboutPath,
|
||||||
|
headersPath,
|
||||||
tokenFile,
|
tokenFile,
|
||||||
otherArgs
|
otherArgs
|
||||||
)
|
)
|
||||||
@@ -47,6 +54,43 @@ export const processProgram = async (
|
|||||||
// copy the code.js program to log and end write stream
|
// copy the code.js program to log and end write stream
|
||||||
writeStream.end(program)
|
writeStream.end(program)
|
||||||
|
|
||||||
|
session.completed = true
|
||||||
|
console.log('session completed', session)
|
||||||
|
} catch (err: any) {
|
||||||
|
session.completed = true
|
||||||
|
session.crashed = err.toString()
|
||||||
|
console.log('session crashed', session.id, session.crashed)
|
||||||
|
}
|
||||||
|
} else if (runTime === RunTimeType.PY) {
|
||||||
|
program = await createPythonProgram(
|
||||||
|
program,
|
||||||
|
preProgramVariables,
|
||||||
|
vars,
|
||||||
|
session,
|
||||||
|
weboutPath,
|
||||||
|
headersPath,
|
||||||
|
tokenFile,
|
||||||
|
otherArgs
|
||||||
|
)
|
||||||
|
|
||||||
|
const codePath = path.join(session.path, 'code.py')
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createFile(codePath, program)
|
||||||
|
|
||||||
|
// create a stream that will write to console outputs to log file
|
||||||
|
const writeStream = fs.createWriteStream(logPath)
|
||||||
|
|
||||||
|
// waiting for the open event so that we can have underlying file descriptor
|
||||||
|
await once(writeStream, 'open')
|
||||||
|
|
||||||
|
execFileSync(process.pythonLoc!, [codePath], {
|
||||||
|
stdio: ['ignore', writeStream, writeStream]
|
||||||
|
})
|
||||||
|
|
||||||
|
// copy the code.py program to log and end write stream
|
||||||
|
writeStream.end(program)
|
||||||
|
|
||||||
session.completed = true
|
session.completed = true
|
||||||
console.log('session completed', session)
|
console.log('session completed', session)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
|||||||
@@ -1,33 +1,16 @@
|
|||||||
import express from 'express'
|
import express from 'express'
|
||||||
import {
|
import { Request, Security, Route, Tags, Post, Body, Get, Query } from 'tsoa'
|
||||||
Request,
|
import { ExecutionController, ExecutionVars } from './internal'
|
||||||
Security,
|
|
||||||
Route,
|
|
||||||
Tags,
|
|
||||||
Post,
|
|
||||||
Body,
|
|
||||||
Get,
|
|
||||||
Query,
|
|
||||||
Example
|
|
||||||
} from 'tsoa'
|
|
||||||
import {
|
|
||||||
ExecuteReturnJson,
|
|
||||||
ExecuteReturnRaw,
|
|
||||||
ExecutionController,
|
|
||||||
ExecutionVars
|
|
||||||
} from './internal'
|
|
||||||
import {
|
import {
|
||||||
getPreProgramVariables,
|
getPreProgramVariables,
|
||||||
HTTPHeaders,
|
HTTPHeaders,
|
||||||
isDebugOn,
|
|
||||||
LogLine,
|
LogLine,
|
||||||
makeFilesNamesMap,
|
makeFilesNamesMap,
|
||||||
parseLogToArray,
|
|
||||||
getRunTimeAndFilePath
|
getRunTimeAndFilePath
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { MulterFile } from '../types/Upload'
|
import { MulterFile } from '../types/Upload'
|
||||||
|
|
||||||
interface ExecuteReturnJsonPayload {
|
interface ExecutePostRequestPayload {
|
||||||
/**
|
/**
|
||||||
* Location of SAS program
|
* Location of SAS program
|
||||||
* @example "/Public/somefolder/some.file"
|
* @example "/Public/somefolder/some.file"
|
||||||
@@ -35,17 +18,6 @@ interface ExecuteReturnJsonPayload {
|
|||||||
_program?: string
|
_program?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IRecordOfAny {
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
export interface ExecuteReturnJsonResponse {
|
|
||||||
status: string
|
|
||||||
_webout: string | IRecordOfAny
|
|
||||||
log: LogLine[]
|
|
||||||
message?: string
|
|
||||||
httpHeaders: HTTPHeaders
|
|
||||||
}
|
|
||||||
|
|
||||||
@Security('bearerAuth')
|
@Security('bearerAuth')
|
||||||
@Route('SASjsApi/stp')
|
@Route('SASjsApi/stp')
|
||||||
@Tags('STP')
|
@Tags('STP')
|
||||||
@@ -62,11 +34,12 @@ export class STPController {
|
|||||||
* @example _program "/Projects/myApp/some/program"
|
* @example _program "/Projects/myApp/some/program"
|
||||||
*/
|
*/
|
||||||
@Get('/execute')
|
@Get('/execute')
|
||||||
public async executeReturnRaw(
|
public async executeGetRequest(
|
||||||
@Request() request: express.Request,
|
@Request() request: express.Request,
|
||||||
@Query() _program: string
|
@Query() _program: string
|
||||||
): Promise<string | Buffer> {
|
): Promise<string | Buffer> {
|
||||||
return executeReturnRaw(request, _program)
|
const vars = request.query as ExecutionVars
|
||||||
|
return execute(request, _program, vars)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -87,50 +60,42 @@ export class STPController {
|
|||||||
* @param _program Location of SAS or JS code
|
* @param _program Location of SAS or JS code
|
||||||
* @example _program "/Projects/myApp/some/program"
|
* @example _program "/Projects/myApp/some/program"
|
||||||
*/
|
*/
|
||||||
@Example<ExecuteReturnJsonResponse>({
|
|
||||||
status: 'success',
|
|
||||||
_webout: 'webout content',
|
|
||||||
log: [],
|
|
||||||
httpHeaders: {
|
|
||||||
'Content-type': 'application/zip',
|
|
||||||
'Cache-Control': 'public, max-age=1000'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@Post('/execute')
|
@Post('/execute')
|
||||||
public async executeReturnJson(
|
public async executePostRequest(
|
||||||
@Request() request: express.Request,
|
@Request() request: express.Request,
|
||||||
@Body() body?: ExecuteReturnJsonPayload,
|
@Body() body?: ExecutePostRequestPayload,
|
||||||
@Query() _program?: string
|
@Query() _program?: string
|
||||||
): Promise<ExecuteReturnJsonResponse> {
|
): Promise<string | Buffer> {
|
||||||
const program = _program ?? body?._program
|
const program = _program ?? body?._program
|
||||||
return executeReturnJson(request, program!)
|
const vars = { ...request.query, ...request.body }
|
||||||
|
const filesNamesMap = request.files?.length
|
||||||
|
? makeFilesNamesMap(request.files as MulterFile[])
|
||||||
|
: null
|
||||||
|
const otherArgs = { filesNamesMap: filesNamesMap }
|
||||||
|
|
||||||
|
return execute(request, program!, vars, otherArgs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const executeReturnRaw = async (
|
const execute = async (
|
||||||
req: express.Request,
|
req: express.Request,
|
||||||
_program: string
|
_program: string,
|
||||||
|
vars: ExecutionVars,
|
||||||
|
otherArgs?: any
|
||||||
): Promise<string | Buffer> => {
|
): Promise<string | Buffer> => {
|
||||||
const query = req.query as ExecutionVars
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
|
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
|
||||||
|
|
||||||
const { result, httpHeaders } =
|
const { result, httpHeaders } = await new ExecutionController().executeFile(
|
||||||
(await new ExecutionController().executeFile({
|
{
|
||||||
programPath: codePath,
|
programPath: codePath,
|
||||||
|
runTime,
|
||||||
preProgramVariables: getPreProgramVariables(req),
|
preProgramVariables: getPreProgramVariables(req),
|
||||||
vars: query,
|
vars,
|
||||||
runTime
|
otherArgs,
|
||||||
})) as ExecuteReturnRaw
|
session: req.sasjsSession
|
||||||
|
}
|
||||||
// Should over-ride response header for debug
|
)
|
||||||
// on GET request to see entire log rendering on browser.
|
|
||||||
if (isDebugOn(query)) {
|
|
||||||
httpHeaders['content-type'] = 'text/plain'
|
|
||||||
}
|
|
||||||
|
|
||||||
req.res?.set(httpHeaders)
|
|
||||||
|
|
||||||
if (result instanceof Buffer) {
|
if (result instanceof Buffer) {
|
||||||
;(req as any).sasHeaders = httpHeaders
|
;(req as any).sasHeaders = httpHeaders
|
||||||
@@ -146,48 +111,3 @@ const executeReturnRaw = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const executeReturnJson = async (
|
|
||||||
req: express.Request,
|
|
||||||
_program: string
|
|
||||||
): Promise<ExecuteReturnJsonResponse> => {
|
|
||||||
const filesNamesMap = req.files?.length
|
|
||||||
? makeFilesNamesMap(req.files as MulterFile[])
|
|
||||||
: null
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
|
|
||||||
|
|
||||||
const { webout, log, httpHeaders } =
|
|
||||||
(await new ExecutionController().executeFile({
|
|
||||||
programPath: codePath,
|
|
||||||
preProgramVariables: getPreProgramVariables(req),
|
|
||||||
vars: { ...req.query, ...req.body },
|
|
||||||
otherArgs: { filesNamesMap: filesNamesMap },
|
|
||||||
returnJson: true,
|
|
||||||
session: req.sasjsSession,
|
|
||||||
runTime
|
|
||||||
})) as ExecuteReturnJson
|
|
||||||
|
|
||||||
let weboutRes: string | IRecordOfAny = webout
|
|
||||||
if (httpHeaders['content-type']?.toLowerCase() === 'application/json') {
|
|
||||||
try {
|
|
||||||
weboutRes = JSON.parse(webout as string)
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: 'success',
|
|
||||||
_webout: weboutRes,
|
|
||||||
log: parseLogToArray(log),
|
|
||||||
httpHeaders
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
throw {
|
|
||||||
code: 400,
|
|
||||||
status: 'failure',
|
|
||||||
message: 'Job execution failed.',
|
|
||||||
error: typeof err === 'object' ? err.toString() : err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ import {
|
|||||||
import { createFile, generateTimestamp, deleteFolder } from '@sasjs/utils'
|
import { createFile, generateTimestamp, deleteFolder } from '@sasjs/utils'
|
||||||
import {
|
import {
|
||||||
SASSessionController,
|
SASSessionController,
|
||||||
JSSessionController
|
JSSessionController,
|
||||||
|
PythonSessionController
|
||||||
} from '../../../controllers/internal'
|
} from '../../../controllers/internal'
|
||||||
import * as ProcessProgramModule from '../../../controllers/internal/processProgram'
|
import * as ProcessProgramModule from '../../../controllers/internal/processProgram'
|
||||||
import { Session } from '../../../types'
|
import { Session } from '../../../types'
|
||||||
@@ -39,14 +40,17 @@ const user = {
|
|||||||
|
|
||||||
const sampleSasProgram = '%put hello world!;'
|
const sampleSasProgram = '%put hello world!;'
|
||||||
const sampleJsProgram = `console.log('hello world!/')`
|
const sampleJsProgram = `console.log('hello world!/')`
|
||||||
|
const samplePyProgram = `print('hello world!/')`
|
||||||
|
|
||||||
const filesFolder = getFilesFolder()
|
const filesFolder = getFilesFolder()
|
||||||
|
const testFilesFolder = `test-stp-${generateTimestamp()}`
|
||||||
|
|
||||||
|
let app: Express
|
||||||
|
let accessToken: string
|
||||||
|
|
||||||
describe('stp', () => {
|
describe('stp', () => {
|
||||||
let app: Express
|
|
||||||
let con: Mongoose
|
let con: Mongoose
|
||||||
let mongoServer: MongoMemoryServer
|
let mongoServer: MongoMemoryServer
|
||||||
let accessToken: string
|
|
||||||
const userController = new UserController()
|
const userController = new UserController()
|
||||||
const permissionController = new PermissionController()
|
const permissionController = new PermissionController()
|
||||||
|
|
||||||
@@ -72,8 +76,6 @@ describe('stp', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('execute', () => {
|
describe('execute', () => {
|
||||||
const testFilesFolder = `test-stp-${generateTimestamp()}`
|
|
||||||
|
|
||||||
describe('get', () => {
|
describe('get', () => {
|
||||||
describe('with runtime js', () => {
|
describe('with runtime js', () => {
|
||||||
const testFilesFolder = `test-stp-${generateTimestamp()}`
|
const testFilesFolder = `test-stp-${generateTimestamp()}`
|
||||||
@@ -93,41 +95,45 @@ describe('stp', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should execute js program when both js and sas program are present', async () => {
|
it('should execute js program when both js and sas program are present', async () => {
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
await makeRequestAndAssert(
|
||||||
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
|
[RunTimeType.JS, RunTimeType.SAS],
|
||||||
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
|
200,
|
||||||
await createFile(sasProgramPath, sampleSasProgram)
|
RunTimeType.JS
|
||||||
await createFile(jsProgramPath, sampleJsProgram)
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
RunTimeType.JS,
|
|
||||||
expect.anything(),
|
|
||||||
undefined
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw error when js program is not present but sas program exists', async () => {
|
it('should throw error when js program is not present but sas program exists', async () => {
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
await makeRequestAndAssert([], 400)
|
||||||
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
|
})
|
||||||
await createFile(sasProgramPath, sampleSasProgram)
|
})
|
||||||
|
|
||||||
await request(app)
|
describe('with runtime py', () => {
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
const testFilesFolder = `test-stp-${generateTimestamp()}`
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
beforeAll(() => {
|
||||||
.expect(400)
|
process.runTimes = [RunTimeType.PY]
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules() // it clears the cache
|
||||||
|
setupMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.resetAllMocks()
|
||||||
|
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute python program when python, js and sas programs are present', async () => {
|
||||||
|
await makeRequestAndAssert(
|
||||||
|
[RunTimeType.PY, RunTimeType.SAS, RunTimeType.JS],
|
||||||
|
200,
|
||||||
|
RunTimeType.PY
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error when py program is not present but js or sas program exists', async () => {
|
||||||
|
await makeRequestAndAssert([], 400)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -147,41 +153,11 @@ describe('stp', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should execute sas program when both sas and js programs are present', async () => {
|
it('should execute sas program when both sas and js programs are present', async () => {
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
await makeRequestAndAssert([RunTimeType.SAS], 200, RunTimeType.SAS)
|
||||||
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
|
|
||||||
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
|
|
||||||
await createFile(sasProgramPath, sampleSasProgram)
|
|
||||||
await createFile(jsProgramPath, sampleJsProgram)
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
RunTimeType.SAS,
|
|
||||||
expect.anything(),
|
|
||||||
undefined
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw error when sas program do not exit but js exists', async () => {
|
it('should throw error when sas program do not exit but js exists', async () => {
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
await makeRequestAndAssert([], 400)
|
||||||
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
|
|
||||||
await createFile(jsProgramPath, sampleJsProgram)
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(400)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -201,63 +177,51 @@ describe('stp', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should execute js program when both js and sas program are present', async () => {
|
it('should execute js program when both js and sas program are present', async () => {
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
await makeRequestAndAssert(
|
||||||
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
|
[RunTimeType.SAS, RunTimeType.JS],
|
||||||
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
|
200,
|
||||||
await createFile(sasProgramPath, sampleSasProgram)
|
RunTimeType.JS
|
||||||
await createFile(jsProgramPath, sampleJsProgram)
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
RunTimeType.JS,
|
|
||||||
expect.anything(),
|
|
||||||
undefined
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should execute sas program when js program is not present but sas program exists', async () => {
|
it('should execute sas program when js program is not present but sas program exists', async () => {
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
await makeRequestAndAssert([RunTimeType.SAS], 200, RunTimeType.SAS)
|
||||||
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
|
|
||||||
await createFile(sasProgramPath, sampleSasProgram)
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
RunTimeType.SAS,
|
|
||||||
expect.anything(),
|
|
||||||
undefined
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw error when both sas and js programs do not exist', async () => {
|
it('should throw error when both sas and js programs do not exist', async () => {
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
await makeRequestAndAssert([], 400)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
await request(app)
|
describe('with runtime py and sas', () => {
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
beforeAll(() => {
|
||||||
.auth(accessToken, { type: 'bearer' })
|
process.runTimes = [RunTimeType.PY, RunTimeType.SAS]
|
||||||
.send()
|
})
|
||||||
.expect(400)
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules() // it clears the cache
|
||||||
|
setupMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.resetAllMocks()
|
||||||
|
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute python program when both python and sas program are present', async () => {
|
||||||
|
await makeRequestAndAssert(
|
||||||
|
[RunTimeType.PY, RunTimeType.SAS],
|
||||||
|
200,
|
||||||
|
RunTimeType.PY
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute sas program when python program is not present but sas program exists', async () => {
|
||||||
|
await makeRequestAndAssert([RunTimeType.SAS], 200, RunTimeType.SAS)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error when both sas and js programs do not exist', async () => {
|
||||||
|
await makeRequestAndAssert([], 400)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -277,76 +241,220 @@ describe('stp', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should execute sas program when both sas and js programs exist', async () => {
|
it('should execute sas program when both sas and js programs exist', async () => {
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
await makeRequestAndAssert(
|
||||||
const sasProgramPath = path.join(filesFolder, `${programPath}.sas`)
|
[RunTimeType.SAS, RunTimeType.JS],
|
||||||
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
|
200,
|
||||||
await createFile(sasProgramPath, sampleSasProgram)
|
RunTimeType.SAS
|
||||||
await createFile(jsProgramPath, sampleJsProgram)
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
RunTimeType.SAS,
|
|
||||||
expect.anything(),
|
|
||||||
undefined
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should execute js program when sas program is not present but js program exists', async () => {
|
it('should execute js program when sas program is not present but js program exists', async () => {
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
await makeRequestAndAssert([RunTimeType.JS], 200, RunTimeType.JS)
|
||||||
const jsProgramPath = path.join(filesFolder, `${programPath}.js`)
|
|
||||||
await createFile(jsProgramPath, sampleJsProgram)
|
|
||||||
|
|
||||||
await request(app)
|
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
|
||||||
.auth(accessToken, { type: 'bearer' })
|
|
||||||
.send()
|
|
||||||
.expect(200)
|
|
||||||
|
|
||||||
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
RunTimeType.JS,
|
|
||||||
expect.anything(),
|
|
||||||
undefined
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should throw error when both sas and js programs do not exist', async () => {
|
it('should throw error when both sas and js programs do not exist', async () => {
|
||||||
const programPath = path.join(testFilesFolder, 'program')
|
await makeRequestAndAssert([], 400)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
await request(app)
|
describe('with runtime sas and py', () => {
|
||||||
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
beforeAll(() => {
|
||||||
.auth(accessToken, { type: 'bearer' })
|
process.runTimes = [RunTimeType.SAS, RunTimeType.PY]
|
||||||
.send()
|
})
|
||||||
.expect(400)
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules() // it clears the cache
|
||||||
|
setupMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.resetAllMocks()
|
||||||
|
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute sas program when both sas and python programs exist', async () => {
|
||||||
|
await makeRequestAndAssert(
|
||||||
|
[RunTimeType.SAS, RunTimeType.PY],
|
||||||
|
200,
|
||||||
|
RunTimeType.SAS
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute python program when sas program is not present but python program exists', async () => {
|
||||||
|
await makeRequestAndAssert([RunTimeType.PY], 200, RunTimeType.PY)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error when both sas and python programs do not exist', async () => {
|
||||||
|
await makeRequestAndAssert([], 400)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with runtime sas, js and py', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
process.runTimes = [RunTimeType.SAS, RunTimeType.JS, RunTimeType.PY]
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules() // it clears the cache
|
||||||
|
setupMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.resetAllMocks()
|
||||||
|
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute sas program when it exists, no matter js and python programs exist or not', async () => {
|
||||||
|
await makeRequestAndAssert(
|
||||||
|
[RunTimeType.SAS, RunTimeType.PY, RunTimeType.JS],
|
||||||
|
200,
|
||||||
|
RunTimeType.SAS
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute js program when sas program is absent but js and python programs are present', async () => {
|
||||||
|
await makeRequestAndAssert(
|
||||||
|
[RunTimeType.JS, RunTimeType.PY],
|
||||||
|
200,
|
||||||
|
RunTimeType.JS
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute python program when both sas and js programs are not present', async () => {
|
||||||
|
await makeRequestAndAssert([RunTimeType.PY], 200, RunTimeType.PY)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error when no program exists', async () => {
|
||||||
|
await makeRequestAndAssert([], 400)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with runtime js, sas and py', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
process.runTimes = [RunTimeType.JS, RunTimeType.SAS, RunTimeType.PY]
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules() // it clears the cache
|
||||||
|
setupMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.resetAllMocks()
|
||||||
|
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute js program when it exists, no matter sas and python programs exist or not', async () => {
|
||||||
|
await makeRequestAndAssert(
|
||||||
|
[RunTimeType.JS, RunTimeType.SAS, RunTimeType.PY],
|
||||||
|
200,
|
||||||
|
RunTimeType.JS
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute sas program when js program is absent but sas and python programs are present', async () => {
|
||||||
|
await makeRequestAndAssert(
|
||||||
|
[RunTimeType.SAS, RunTimeType.PY],
|
||||||
|
200,
|
||||||
|
RunTimeType.SAS
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute python program when both sas and js programs are not present', async () => {
|
||||||
|
await makeRequestAndAssert([RunTimeType.PY], 200, RunTimeType.PY)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error when no program exists', async () => {
|
||||||
|
await makeRequestAndAssert([], 400)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with runtime py, sas and js', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
process.runTimes = [RunTimeType.PY, RunTimeType.SAS, RunTimeType.JS]
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.resetModules() // it clears the cache
|
||||||
|
setupMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.resetAllMocks()
|
||||||
|
await deleteFolder(path.join(filesFolder, testFilesFolder))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute python program when it exists, no matter sas and js programs exist or not', async () => {
|
||||||
|
await makeRequestAndAssert(
|
||||||
|
[RunTimeType.PY, RunTimeType.SAS, RunTimeType.JS],
|
||||||
|
200,
|
||||||
|
RunTimeType.PY
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute sas program when python program is absent but sas and js programs are present', async () => {
|
||||||
|
await makeRequestAndAssert(
|
||||||
|
[RunTimeType.SAS, RunTimeType.JS],
|
||||||
|
200,
|
||||||
|
RunTimeType.SAS
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should execute js program when both sas and python programs are not present', async () => {
|
||||||
|
await makeRequestAndAssert([RunTimeType.JS], 200, RunTimeType.JS)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw error when no program exists', async () => {
|
||||||
|
await makeRequestAndAssert([], 400)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const generateSaveTokenAndCreateUser = async (
|
const makeRequestAndAssert = async (
|
||||||
someUser: any
|
programTypes: RunTimeType[],
|
||||||
): Promise<string> => {
|
expectedStatusCode: number,
|
||||||
const userController = new UserController()
|
expectedRuntime?: RunTimeType
|
||||||
const dbUser = await userController.createUser(someUser)
|
) => {
|
||||||
|
const programPath = path.join(testFilesFolder, 'program')
|
||||||
|
for (const programType of programTypes) {
|
||||||
|
if (programType === RunTimeType.JS)
|
||||||
|
await createFile(
|
||||||
|
path.join(filesFolder, `${programPath}.js`),
|
||||||
|
sampleJsProgram
|
||||||
|
)
|
||||||
|
else if (programType === RunTimeType.PY)
|
||||||
|
await createFile(
|
||||||
|
path.join(filesFolder, `${programPath}.py`),
|
||||||
|
samplePyProgram
|
||||||
|
)
|
||||||
|
else if (programType === RunTimeType.SAS)
|
||||||
|
await createFile(
|
||||||
|
path.join(filesFolder, `${programPath}.sas`),
|
||||||
|
sampleSasProgram
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return generateAndSaveToken(dbUser.id)
|
await request(app)
|
||||||
|
.get(`/SASjsApi/stp/execute?_program=${programPath}`)
|
||||||
|
.auth(accessToken, { type: 'bearer' })
|
||||||
|
.send()
|
||||||
|
.expect(expectedStatusCode)
|
||||||
|
|
||||||
|
if (expectedRuntime)
|
||||||
|
expect(ProcessProgramModule.processProgram).toHaveBeenCalledWith(
|
||||||
|
expect.anything(),
|
||||||
|
expect.anything(),
|
||||||
|
expect.anything(),
|
||||||
|
expect.anything(),
|
||||||
|
expect.anything(),
|
||||||
|
expect.anything(),
|
||||||
|
expect.anything(),
|
||||||
|
expectedRuntime,
|
||||||
|
expect.anything(),
|
||||||
|
undefined
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateAndSaveToken = async (userId: number) => {
|
const generateAndSaveToken = async (userId: number) => {
|
||||||
@@ -367,6 +475,10 @@ const setupMocks = async () => {
|
|||||||
.spyOn(JSSessionController.prototype, 'getSession')
|
.spyOn(JSSessionController.prototype, 'getSession')
|
||||||
.mockImplementation(mockedGetSession)
|
.mockImplementation(mockedGetSession)
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(PythonSessionController.prototype, 'getSession')
|
||||||
|
.mockImplementation(mockedGetSession)
|
||||||
|
|
||||||
jest
|
jest
|
||||||
.spyOn(ProcessProgramModule, 'processProgram')
|
.spyOn(ProcessProgramModule, 'processProgram')
|
||||||
.mockImplementation(() => Promise.resolve())
|
.mockImplementation(() => Promise.resolve())
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ stpRouter.get('/execute', async (req, res) => {
|
|||||||
if (error) return res.status(400).send(error.details[0].message)
|
if (error) return res.status(400).send(error.details[0].message)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.executeReturnRaw(req, query._program)
|
const response = await controller.executeGetRequest(req, query._program)
|
||||||
|
|
||||||
if (response instanceof Buffer) {
|
if (response instanceof Buffer) {
|
||||||
res.writeHead(200, (req as any).sasHeaders)
|
res.writeHead(200, (req as any).sasHeaders)
|
||||||
@@ -42,7 +42,7 @@ stpRouter.post(
|
|||||||
// if (errQ && errB) return res.status(400).send(errB.details[0].message)
|
// if (errQ && errB) return res.status(400).send(errB.details[0].message)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await controller.executeReturnJson(
|
const response = await controller.executePostRequest(
|
||||||
req,
|
req,
|
||||||
req.body,
|
req.body,
|
||||||
req.query?._program as string
|
req.query?._program as string
|
||||||
|
|||||||
3
api/src/types/system/process.d.ts
vendored
3
api/src/types/system/process.d.ts
vendored
@@ -2,10 +2,13 @@ declare namespace NodeJS {
|
|||||||
export interface Process {
|
export interface Process {
|
||||||
sasLoc?: string
|
sasLoc?: string
|
||||||
nodeLoc?: string
|
nodeLoc?: string
|
||||||
|
pythonLoc?: string
|
||||||
driveLoc: string
|
driveLoc: string
|
||||||
logsLoc: string
|
logsLoc: string
|
||||||
|
logsUUID: string
|
||||||
sasSessionController?: import('../../controllers/internal').SASSessionController
|
sasSessionController?: import('../../controllers/internal').SASSessionController
|
||||||
jsSessionController?: import('../../controllers/internal').JSSessionController
|
jsSessionController?: import('../../controllers/internal').JSSessionController
|
||||||
|
pythonSessionController?: import('../../controllers/internal').PythonSessionController
|
||||||
appStreamConfig: import('../').AppStreamConfig
|
appStreamConfig: import('../').AppStreamConfig
|
||||||
logger: import('@sasjs/utils/logger').Logger
|
logger: import('@sasjs/utils/logger').Logger
|
||||||
runTimes: import('../../utils').RunTimeType[]
|
runTimes: import('../../utils').RunTimeType[]
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { createFolder, fileExists, folderExists, isWindows } from '@sasjs/utils'
|
|||||||
import { RunTimeType } from './verifyEnvVariables'
|
import { RunTimeType } from './verifyEnvVariables'
|
||||||
|
|
||||||
export const getDesktopFields = async () => {
|
export const getDesktopFields = async () => {
|
||||||
const { SAS_PATH, NODE_PATH } = process.env
|
const { SAS_PATH, NODE_PATH, PYTHON_PATH } = process.env
|
||||||
|
|
||||||
let sasLoc, nodeLoc
|
let sasLoc, nodeLoc, pythonLoc
|
||||||
|
|
||||||
if (process.runTimes.includes(RunTimeType.SAS)) {
|
if (process.runTimes.includes(RunTimeType.SAS)) {
|
||||||
sasLoc = SAS_PATH ?? (await getSASLocation())
|
sasLoc = SAS_PATH ?? (await getSASLocation())
|
||||||
@@ -16,7 +16,11 @@ export const getDesktopFields = async () => {
|
|||||||
nodeLoc = NODE_PATH ?? (await getNodeLocation())
|
nodeLoc = NODE_PATH ?? (await getNodeLocation())
|
||||||
}
|
}
|
||||||
|
|
||||||
return { sasLoc, nodeLoc }
|
if (process.runTimes.includes(RunTimeType.PY)) {
|
||||||
|
pythonLoc = PYTHON_PATH ?? (await getPythonLocation())
|
||||||
|
}
|
||||||
|
|
||||||
|
return { sasLoc, nodeLoc, pythonLoc }
|
||||||
}
|
}
|
||||||
|
|
||||||
const getDriveLocation = async (): Promise<string> => {
|
const getDriveLocation = async (): Promise<string> => {
|
||||||
@@ -91,3 +95,25 @@ const getNodeLocation = async (): Promise<string> => {
|
|||||||
|
|
||||||
return targetName
|
return targetName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getPythonLocation = async (): Promise<string> => {
|
||||||
|
const validator = async (filePath: string) => {
|
||||||
|
if (!filePath) return 'Path to Python executable is required.'
|
||||||
|
|
||||||
|
if (!(await fileExists(filePath))) {
|
||||||
|
return 'No file found at provided path.'
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultLocation = isWindows() ? 'C:\\Python' : '/usr/bin/python'
|
||||||
|
|
||||||
|
const targetName = await getString(
|
||||||
|
'Please enter full path to a Python executable: ',
|
||||||
|
validator,
|
||||||
|
defaultLocation
|
||||||
|
)
|
||||||
|
|
||||||
|
return targetName
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { RunTimeType } from '.'
|
|||||||
|
|
||||||
export const getRunTimeAndFilePath = async (programPath: string) => {
|
export const getRunTimeAndFilePath = async (programPath: string) => {
|
||||||
const ext = path.extname(programPath)
|
const ext = path.extname(programPath)
|
||||||
// If programPath (_program) is provided with a ".sas" or ".js" extension
|
// If programPath (_program) is provided with a ".sas", ".js" or ".py" extension
|
||||||
// we should use that extension to determine the appropriate runTime
|
// we should use that extension to determine the appropriate runTime
|
||||||
if (ext && Object.values(RunTimeType).includes(ext.slice(1) as RunTimeType)) {
|
if (ext && Object.values(RunTimeType).includes(ext.slice(1) as RunTimeType)) {
|
||||||
const runTime = ext.slice(1)
|
const runTime = ext.slice(1)
|
||||||
|
|||||||
@@ -28,11 +28,13 @@ export const setProcessVariables = async () => {
|
|||||||
if (MODE === ModeType.Server) {
|
if (MODE === ModeType.Server) {
|
||||||
process.sasLoc = process.env.SAS_PATH
|
process.sasLoc = process.env.SAS_PATH
|
||||||
process.nodeLoc = process.env.NODE_PATH
|
process.nodeLoc = process.env.NODE_PATH
|
||||||
|
process.pythonLoc = process.env.PYTHON_PATH
|
||||||
} else {
|
} else {
|
||||||
const { sasLoc, nodeLoc } = await getDesktopFields()
|
const { sasLoc, nodeLoc, pythonLoc } = await getDesktopFields()
|
||||||
|
|
||||||
process.sasLoc = sasLoc
|
process.sasLoc = sasLoc
|
||||||
process.nodeLoc = nodeLoc
|
process.nodeLoc = nodeLoc
|
||||||
|
process.pythonLoc = pythonLoc
|
||||||
}
|
}
|
||||||
|
|
||||||
const { SASJS_ROOT } = process.env
|
const { SASJS_ROOT } = process.env
|
||||||
@@ -48,6 +50,8 @@ export const setProcessVariables = async () => {
|
|||||||
await createFolder(absLogsPath)
|
await createFolder(absLogsPath)
|
||||||
process.logsLoc = getRealPath(absLogsPath)
|
process.logsLoc = getRealPath(absLogsPath)
|
||||||
|
|
||||||
|
process.logsUUID = 'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
|
||||||
|
|
||||||
console.log('sasLoc: ', process.sasLoc)
|
console.log('sasLoc: ', process.sasLoc)
|
||||||
console.log('sasDrive: ', process.driveLoc)
|
console.log('sasDrive: ', process.driveLoc)
|
||||||
console.log('sasLogs: ', process.logsLoc)
|
console.log('sasLogs: ', process.logsLoc)
|
||||||
|
|||||||
@@ -126,9 +126,34 @@ export const generateFileUploadJSCode = async (
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (fileCount) {
|
uploadCode += `\nconst _WEBIN_FILE_COUNT = ${fileCount}`
|
||||||
uploadCode = `\nconst _WEBIN_FILE_COUNT = ${fileCount}` + uploadCode
|
|
||||||
}
|
return uploadCode
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the python code that references uploaded files in the concurrent request
|
||||||
|
* @param filesNamesMap object that maps hashed file names and original file names
|
||||||
|
* @param sessionFolder name of the folder that is created for the purpose of files in concurrent request
|
||||||
|
* @returns generated python code
|
||||||
|
*/
|
||||||
|
export const generateFileUploadPythonCode = async (
|
||||||
|
filesNamesMap: FilenamesMap,
|
||||||
|
sessionFolder: string
|
||||||
|
) => {
|
||||||
|
let uploadCode = ''
|
||||||
|
let fileCount = 0
|
||||||
|
|
||||||
|
const sessionFolderList: string[] = await listFilesInFolder(sessionFolder)
|
||||||
|
sessionFolderList.forEach(async (fileName) => {
|
||||||
|
if (fileName.includes('req_file')) {
|
||||||
|
fileCount++
|
||||||
|
uploadCode += `\n_WEBIN_FILENAME${fileCount} = '${filesNamesMap[fileName].originalName}'`
|
||||||
|
uploadCode += `\n_WEBIN_NAME${fileCount} = '${filesNamesMap[fileName].fieldName}'`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
uploadCode += `\n_WEBIN_FILE_COUNT = ${fileCount}`
|
||||||
|
|
||||||
return uploadCode
|
return uploadCode
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ export enum LOG_FORMAT_MORGANType {
|
|||||||
|
|
||||||
export enum RunTimeType {
|
export enum RunTimeType {
|
||||||
SAS = 'sas',
|
SAS = 'sas',
|
||||||
JS = 'js'
|
JS = 'js',
|
||||||
|
PY = 'py'
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ReturnCode {
|
export enum ReturnCode {
|
||||||
@@ -228,7 +229,7 @@ const verifyRUN_TIMES = (): string[] => {
|
|||||||
|
|
||||||
const verifyExecutablePaths = () => {
|
const verifyExecutablePaths = () => {
|
||||||
const errors: string[] = []
|
const errors: string[] = []
|
||||||
const { RUN_TIMES, SAS_PATH, NODE_PATH, MODE } = process.env
|
const { RUN_TIMES, SAS_PATH, NODE_PATH, PYTHON_PATH, MODE } = process.env
|
||||||
|
|
||||||
if (MODE === ModeType.Server) {
|
if (MODE === ModeType.Server) {
|
||||||
const runTimes = RUN_TIMES?.split(',')
|
const runTimes = RUN_TIMES?.split(',')
|
||||||
@@ -240,6 +241,10 @@ const verifyExecutablePaths = () => {
|
|||||||
if (runTimes?.includes(RunTimeType.JS) && !NODE_PATH) {
|
if (runTimes?.includes(RunTimeType.JS) && !NODE_PATH) {
|
||||||
errors.push(`- NODE_PATH is required for ${RunTimeType.JS} run time`)
|
errors.push(`- NODE_PATH is required for ${RunTimeType.JS} run time`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (runTimes?.includes(RunTimeType.PY) && !PYTHON_PATH) {
|
||||||
|
errors.push(`- PYTHON_PATH is required for ${RunTimeType.PY} run time`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors
|
return errors
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const FilePathInputModal = ({
|
|||||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const value = event.target.value
|
const value = event.target.value
|
||||||
|
|
||||||
const specialChars = /[`!@#$%^&*()_+\-=[\]{};':"\\|,<>?~]/
|
const specialChars = /[`!@#$%^&*()+\-=[\]{};':"\\|,<>?~]/
|
||||||
const fileExtension = /\.(exe|sh|htaccess)$/i
|
const fileExtension = /\.(exe|sh|htaccess)$/i
|
||||||
|
|
||||||
if (specialChars.test(value)) {
|
if (specialChars.test(value)) {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const NameInputModal = ({
|
|||||||
const value = event.target.value
|
const value = event.target.value
|
||||||
|
|
||||||
const folderNameRegex = /[`!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?~]/
|
const folderNameRegex = /[`!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?~]/
|
||||||
const fileNameRegex = /[`!@#$%^&*()_+\-=[\]{};':"\\|,<>/?~]/
|
const fileNameRegex = /[`!@#$%^&*()+\-=[\]{};':"\\|,<>/?~]/
|
||||||
const fileNameExtensionRegex = /.(exe|sh|htaccess)$/i
|
const fileNameExtensionRegex = /.(exe|sh|htaccess)$/i
|
||||||
|
|
||||||
const specialChars = isFolder ? folderNameRegex : fileNameRegex
|
const specialChars = isFolder ? folderNameRegex : fileNameRegex
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import React, {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
useContext
|
useContext,
|
||||||
|
useCallback
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
@@ -70,6 +71,8 @@ type SASjsEditorProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const baseUrl = window.location.origin
|
const baseUrl = window.location.origin
|
||||||
|
const SASJS_LOGS_SEPARATOR =
|
||||||
|
'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
|
||||||
|
|
||||||
const SASjsEditor = ({
|
const SASjsEditor = ({
|
||||||
selectedFilePath,
|
selectedFilePath,
|
||||||
@@ -137,6 +140,88 @@ const SASjsEditor = ({
|
|||||||
prevFileContent !== fileContent && !!selectedFilePath
|
prevFileContent !== fileContent && !!selectedFilePath
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const saveFile = useCallback(
|
||||||
|
(filePath?: string) => {
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
if (filePath) {
|
||||||
|
filePath = filePath.startsWith('/') ? filePath : `/${filePath}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData()
|
||||||
|
|
||||||
|
const stringBlob = new Blob([fileContent], { type: 'text/plain' })
|
||||||
|
formData.append('file', stringBlob)
|
||||||
|
formData.append('filePath', filePath ?? selectedFilePath)
|
||||||
|
|
||||||
|
const axiosPromise = filePath
|
||||||
|
? axios.post('/SASjsApi/drive/file', formData)
|
||||||
|
: axios.patch('/SASjsApi/drive/file', formData)
|
||||||
|
|
||||||
|
axiosPromise
|
||||||
|
.then(() => {
|
||||||
|
if (filePath && fileContent === prevFileContent) {
|
||||||
|
// when fileContent and prevFileContent is same,
|
||||||
|
// callback function in setPrevFileContent method is not called
|
||||||
|
// because behind the scene useEffect hook is being used
|
||||||
|
// for calling callback function, and it's only fired when the
|
||||||
|
// new value is not equal to old value.
|
||||||
|
// So, we'll have to explicitly update the selected file path
|
||||||
|
|
||||||
|
setSelectedFilePath(filePath, true)
|
||||||
|
} else {
|
||||||
|
setPrevFileContent(fileContent, () => {
|
||||||
|
if (filePath) {
|
||||||
|
setSelectedFilePath(filePath, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setSnackbarMessage('File saved!')
|
||||||
|
setSnackbarSeverity(AlertSeverityType.Success)
|
||||||
|
setOpenSnackbar(true)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setModalTitle('Abort')
|
||||||
|
setModalPayload(
|
||||||
|
typeof err.response.data === 'object'
|
||||||
|
? JSON.stringify(err.response.data)
|
||||||
|
: err.response.data
|
||||||
|
)
|
||||||
|
setOpenModal(true)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[
|
||||||
|
fileContent,
|
||||||
|
prevFileContent,
|
||||||
|
selectedFilePath,
|
||||||
|
setPrevFileContent,
|
||||||
|
setSelectedFilePath
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
editorRef.current.addAction({
|
||||||
|
// An unique identifier of the contributed action.
|
||||||
|
id: 'save-file',
|
||||||
|
|
||||||
|
// A label of the action that will be presented to the user.
|
||||||
|
label: 'Save',
|
||||||
|
|
||||||
|
// An optional array of keybindings for the action.
|
||||||
|
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS],
|
||||||
|
|
||||||
|
// Method that will be executed when the action is triggered.
|
||||||
|
// @param editor The editor instance is passed in as a convenience
|
||||||
|
run: () => {
|
||||||
|
if (!selectedFilePath) return setOpenFilePathInputModal(true)
|
||||||
|
if (prevFileContent !== fileContent) return saveFile()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [fileContent, prevFileContent, selectedFilePath, saveFile])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRunTimes(Object.values(appContext.runTimes))
|
setRunTimes(Object.values(appContext.runTimes))
|
||||||
}, [appContext.runTimes])
|
}, [appContext.runTimes])
|
||||||
@@ -203,13 +288,8 @@ const SASjsEditor = ({
|
|||||||
axios
|
axios
|
||||||
.post(`/SASjsApi/code/execute`, { code, runTime: selectedRunTime })
|
.post(`/SASjsApi/code/execute`, { code, runTime: selectedRunTime })
|
||||||
.then((res: any) => {
|
.then((res: any) => {
|
||||||
const parsedLog = res?.data?.log
|
setWebout(res.data.split(SASJS_LOGS_SEPARATOR)[0] ?? '')
|
||||||
.map((logLine: any) => logLine.line)
|
setLog(res.data.split(SASJS_LOGS_SEPARATOR)[1] ?? '')
|
||||||
.join('\n')
|
|
||||||
|
|
||||||
setLog(parsedLog)
|
|
||||||
|
|
||||||
setWebout(`${res.data?._webout}`)
|
|
||||||
setTab('log')
|
setTab('log')
|
||||||
|
|
||||||
// Scroll to bottom of log
|
// Scroll to bottom of log
|
||||||
@@ -252,59 +332,6 @@ const SASjsEditor = ({
|
|||||||
saveFile(filePath)
|
saveFile(filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveFile = (filePath?: string) => {
|
|
||||||
setIsLoading(true)
|
|
||||||
|
|
||||||
if (filePath) {
|
|
||||||
filePath = filePath.startsWith('/') ? filePath : `/${filePath}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const formData = new FormData()
|
|
||||||
|
|
||||||
const stringBlob = new Blob([fileContent], { type: 'text/plain' })
|
|
||||||
formData.append('file', stringBlob, 'filename.sas')
|
|
||||||
formData.append('filePath', filePath ?? selectedFilePath)
|
|
||||||
|
|
||||||
const axiosPromise = filePath
|
|
||||||
? axios.post('/SASjsApi/drive/file', formData)
|
|
||||||
: axios.patch('/SASjsApi/drive/file', formData)
|
|
||||||
|
|
||||||
axiosPromise
|
|
||||||
.then(() => {
|
|
||||||
if (filePath && fileContent === prevFileContent) {
|
|
||||||
// when fileContent and prevFileContent is same,
|
|
||||||
// callback function in setPrevFileContent method is not called
|
|
||||||
// because behind the scene useEffect hook is being used
|
|
||||||
// for calling callback function, and it's only fired when the
|
|
||||||
// new value is not equal to old value.
|
|
||||||
// So, we'll have to explicitly update the selected file path
|
|
||||||
|
|
||||||
setSelectedFilePath(filePath, true)
|
|
||||||
} else {
|
|
||||||
setPrevFileContent(fileContent, () => {
|
|
||||||
if (filePath) {
|
|
||||||
setSelectedFilePath(filePath, true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
setSnackbarMessage('File saved!')
|
|
||||||
setSnackbarSeverity(AlertSeverityType.Success)
|
|
||||||
setOpenSnackbar(true)
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setModalTitle('Abort')
|
|
||||||
setModalPayload(
|
|
||||||
typeof err.response.data === 'object'
|
|
||||||
? JSON.stringify(err.response.data)
|
|
||||||
: err.response.data
|
|
||||||
)
|
|
||||||
setOpenModal(true)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsLoading(false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ width: '100%', typography: 'body1', marginTop: '50px' }}>
|
<Box sx={{ width: '100%', typography: 'body1', marginTop: '50px' }}>
|
||||||
<Backdrop
|
<Backdrop
|
||||||
|
|||||||
Reference in New Issue
Block a user