@jsreport/jsreport-core 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/main/logger.js +21 -11
- package/lib/main/monitoring.js +2 -1
- package/lib/main/profiler.js +13 -1
- package/lib/main/reporter.js +33 -14
- package/lib/worker/render/executeEngine.js +0 -7
- package/lib/worker/render/profiler.js +14 -10
- package/lib/worker/reporter.js +5 -0
- package/lib/worker/sandbox/runInSandbox.js +48 -3
- package/lib/worker/sandbox/safeSandbox.js +3 -2
- package/package.json +1 -2
package/lib/main/logger.js
CHANGED
|
@@ -37,14 +37,25 @@ function createLogger () {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
function configureLogger (logger, _transports) {
|
|
40
|
+
const transports = _transports || {}
|
|
41
|
+
const transportFormatMap = new WeakMap()
|
|
42
|
+
|
|
43
|
+
// we ensure we do .format cleanup on options first before checking if the logger
|
|
44
|
+
// is configured or not, this ensure that options are properly cleaned up when
|
|
45
|
+
// configureLogger is called more than once (like when execution cli commands from extensions)
|
|
46
|
+
for (const [, transpOptions] of Object.entries(transports)) {
|
|
47
|
+
if (transpOptions.format != null) {
|
|
48
|
+
transportFormatMap.set(transpOptions, transpOptions.format)
|
|
49
|
+
delete transpOptions.format
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
40
53
|
const configuredPreviously = logger.__configured__ === true
|
|
41
54
|
|
|
42
55
|
if (configuredPreviously) {
|
|
43
56
|
return
|
|
44
57
|
}
|
|
45
58
|
|
|
46
|
-
const transports = _transports || {}
|
|
47
|
-
|
|
48
59
|
const knownTransports = {
|
|
49
60
|
debug: DebugTransport,
|
|
50
61
|
console: winston.transports.Console,
|
|
@@ -78,22 +89,21 @@ function configureLogger (logger, _transports) {
|
|
|
78
89
|
continue
|
|
79
90
|
}
|
|
80
91
|
|
|
92
|
+
let originalFormat
|
|
93
|
+
|
|
94
|
+
if (transportFormatMap.has(transpOptions)) {
|
|
95
|
+
originalFormat = transportFormatMap.get(transpOptions)
|
|
96
|
+
}
|
|
97
|
+
|
|
81
98
|
if (
|
|
82
|
-
|
|
83
|
-
typeof
|
|
99
|
+
originalFormat != null &&
|
|
100
|
+
typeof originalFormat.constructor !== 'function'
|
|
84
101
|
) {
|
|
85
102
|
throw new Error(`Invalid option for transport object "${
|
|
86
103
|
transpName
|
|
87
104
|
}", option "format" has an incorrect value, must be an instance of loggerFormat. check your "logger" config`)
|
|
88
105
|
}
|
|
89
106
|
|
|
90
|
-
let originalFormat
|
|
91
|
-
|
|
92
|
-
if (transpOptions.format != null) {
|
|
93
|
-
originalFormat = transpOptions.format
|
|
94
|
-
delete transpOptions.format
|
|
95
|
-
}
|
|
96
|
-
|
|
97
107
|
const options = Object.assign(omit(transpOptions, knownOptions), {
|
|
98
108
|
name: transpName
|
|
99
109
|
})
|
package/lib/main/monitoring.js
CHANGED
package/lib/main/profiler.js
CHANGED
|
@@ -26,9 +26,13 @@ module.exports = (reporter) => {
|
|
|
26
26
|
return
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
let lastOperation
|
|
30
|
+
|
|
29
31
|
for (const m of events) {
|
|
30
32
|
if (m.type === 'log') {
|
|
31
33
|
reporter.logger[m.level](m.message, { ...req, ...m.meta, timestamp: m.timestamp })
|
|
34
|
+
} else {
|
|
35
|
+
lastOperation = m
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
if (profilersMap.has(req.context.rootId)) {
|
|
@@ -36,8 +40,15 @@ module.exports = (reporter) => {
|
|
|
36
40
|
}
|
|
37
41
|
}
|
|
38
42
|
|
|
43
|
+
if (lastOperation != null) {
|
|
44
|
+
req.context.profiling.lastOperation = lastOperation
|
|
45
|
+
}
|
|
46
|
+
|
|
39
47
|
profilerAppendChain.set(req.context.rootId, profilerAppendChain.get(req.context.rootId).then(() => {
|
|
40
|
-
return reporter.blobStorage.append(
|
|
48
|
+
return reporter.blobStorage.append(
|
|
49
|
+
req.context.profiling.entity.blobName,
|
|
50
|
+
Buffer.from(events.map(m => JSON.stringify(m)).join('\n') + '\n'), req
|
|
51
|
+
).catch(e => {
|
|
41
52
|
reporter.logger.error('Failed to append to profile blob', e)
|
|
42
53
|
})
|
|
43
54
|
}))
|
|
@@ -64,6 +75,7 @@ module.exports = (reporter) => {
|
|
|
64
75
|
profilerAppendChain.set(req.context.rootId, Promise.resolve())
|
|
65
76
|
|
|
66
77
|
req.context.profiling = req.context.profiling || {}
|
|
78
|
+
req.context.profiling.lastOperation = null
|
|
67
79
|
|
|
68
80
|
let blobName = `profiles/${req.context.rootId}.log`
|
|
69
81
|
|
package/lib/main/reporter.js
CHANGED
|
@@ -320,26 +320,29 @@ class MainReporter extends Reporter {
|
|
|
320
320
|
throw new Error('Not initialized, you need to call jsreport.init().then before rendering')
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
+
let options = {}
|
|
324
|
+
if (parentReq && !parentReq.__isJsreportRequest__) {
|
|
325
|
+
options = parentReq
|
|
326
|
+
parentReq = null
|
|
327
|
+
}
|
|
328
|
+
|
|
323
329
|
req = Object.assign({}, req)
|
|
324
330
|
req.context = Object.assign({}, req.context)
|
|
325
331
|
req.context.rootId = req.context.rootId || generateRequestId()
|
|
326
332
|
req.context.id = req.context.rootId
|
|
327
333
|
|
|
328
|
-
const worker = await this._workersManager.allocate(req, {
|
|
334
|
+
const worker = options.worker || await this._workersManager.allocate(req, {
|
|
329
335
|
timeout: this.options.reportTimeout
|
|
330
336
|
})
|
|
331
337
|
|
|
338
|
+
let keepWorker
|
|
332
339
|
let workerAborted
|
|
333
|
-
if (parentReq && !parentReq.__isJsreportRequest__) {
|
|
334
|
-
const options = parentReq
|
|
335
|
-
parentReq = null
|
|
336
340
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
}
|
|
341
|
+
if (options.abortEmitter) {
|
|
342
|
+
options.abortEmitter.once('abort', () => {
|
|
343
|
+
workerAborted = true
|
|
344
|
+
worker.release(req).catch((e) => this.logger.error('Failed to release worker ' + e))
|
|
345
|
+
})
|
|
343
346
|
}
|
|
344
347
|
|
|
345
348
|
const res = { meta: {} }
|
|
@@ -388,12 +391,19 @@ class MainReporter extends Reporter {
|
|
|
388
391
|
req.options &&
|
|
389
392
|
req.options.timeout != null
|
|
390
393
|
) {
|
|
391
|
-
reportTimeout = req
|
|
394
|
+
reportTimeout = req.options.timeout
|
|
392
395
|
}
|
|
393
396
|
|
|
394
|
-
await this.beforeRenderListeners.fire(req, res)
|
|
397
|
+
await this.beforeRenderListeners.fire(req, res, { worker })
|
|
395
398
|
|
|
396
|
-
|
|
399
|
+
// this is used so far just in the reports extension
|
|
400
|
+
// it wants to send to the client immediate response with link to the report status
|
|
401
|
+
// but the previous steps already allocated worker which has the parsed input request
|
|
402
|
+
// so we need to keep the worker active and let the subsequent real render call use it
|
|
403
|
+
// we cant move the main beforeRenderListener before the worker allocation, because at that point
|
|
404
|
+
// the request isn't parsed and we don't know the template and options
|
|
405
|
+
if (req.context.returnResponseAndKeepWorker) {
|
|
406
|
+
keepWorker = true
|
|
397
407
|
res.stream = Readable.from(res.content)
|
|
398
408
|
return res
|
|
399
409
|
}
|
|
@@ -414,6 +424,15 @@ class MainReporter extends Reporter {
|
|
|
414
424
|
} catch (err) {
|
|
415
425
|
if (err.code === 'WORKER_TIMEOUT') {
|
|
416
426
|
err.message = 'Report timeout'
|
|
427
|
+
if (req.context.profiling?.lastOperation != null && req.context.profiling?.entity != null) {
|
|
428
|
+
err.message += `. Last profiler operation: (${req.context.profiling.lastOperation.subtype}) ${req.context.profiling.lastOperation.name}`
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (req.context.http != null) {
|
|
432
|
+
const profileUrl = `${req.context.http.baseUrl}/studio/profiles/${req.context.profiling.entity._id}`
|
|
433
|
+
err.message += `. You can inspect and find more details here: ${profileUrl}`
|
|
434
|
+
}
|
|
435
|
+
|
|
417
436
|
err.weak = true
|
|
418
437
|
}
|
|
419
438
|
|
|
@@ -429,7 +448,7 @@ class MainReporter extends Reporter {
|
|
|
429
448
|
await this.renderErrorListeners.fire(req, res, err)
|
|
430
449
|
throw err
|
|
431
450
|
} finally {
|
|
432
|
-
if (!workerAborted) {
|
|
451
|
+
if (!workerAborted && !keepWorker) {
|
|
433
452
|
await worker.release(req)
|
|
434
453
|
}
|
|
435
454
|
}
|
|
@@ -85,7 +85,6 @@ module.exports = (reporter) => {
|
|
|
85
85
|
|
|
86
86
|
if (entity._id) {
|
|
87
87
|
entityPath = await reporter.folders.resolveEntityPath(entity, entitySet, req)
|
|
88
|
-
entityPath = entityPath.substring(0, entityPath.lastIndexOf('/'))
|
|
89
88
|
}
|
|
90
89
|
|
|
91
90
|
const registerResults = await reporter.registerHelpersListeners.fire(req)
|
|
@@ -106,12 +105,6 @@ module.exports = (reporter) => {
|
|
|
106
105
|
const executionFnParsedParamsKey = `entity:${entity.shortid || 'anonymous'}:helpers:${joinedHelpers}`
|
|
107
106
|
|
|
108
107
|
const executionFn = async ({ require, console, topLevelFunctions }) => {
|
|
109
|
-
// cached components dont call runInSandbox but share the proxy, we get to it here
|
|
110
|
-
if (entitySet !== 'templates') {
|
|
111
|
-
const jsreport = require('jsreport-proxy')
|
|
112
|
-
jsreport.currentPath = entityPath
|
|
113
|
-
}
|
|
114
|
-
|
|
115
108
|
const asyncResultMap = new Map()
|
|
116
109
|
executionFnParsedParamsMap.get(req.context.id).get(executionFnParsedParamsKey).resolve({ require, console, topLevelFunctions })
|
|
117
110
|
const key = `template:${content}:${engine.name}`
|
|
@@ -18,16 +18,7 @@ class Profiler {
|
|
|
18
18
|
})
|
|
19
19
|
|
|
20
20
|
this.profiledRequestsMap = new Map()
|
|
21
|
-
const profileEventsFlushInterval = setInterval(
|
|
22
|
-
for (const id of [...this.profiledRequestsMap.keys()]) {
|
|
23
|
-
const profilingInfo = this.profiledRequestsMap.get(id)
|
|
24
|
-
if (profilingInfo) {
|
|
25
|
-
const batch = profilingInfo.batch
|
|
26
|
-
profilingInfo.batch = []
|
|
27
|
-
await this.reporter.executeMainAction('profile', batch, profilingInfo.req).catch((e) => this.reporter.logger.error(e, profilingInfo.req))
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}, 100)
|
|
21
|
+
const profileEventsFlushInterval = setInterval(() => this.flush(), 100)
|
|
31
22
|
profileEventsFlushInterval.unref()
|
|
32
23
|
|
|
33
24
|
this.reporter.closeListeners.add('profiler', this, () => {
|
|
@@ -37,6 +28,19 @@ class Profiler {
|
|
|
37
28
|
})
|
|
38
29
|
}
|
|
39
30
|
|
|
31
|
+
async flush (id) {
|
|
32
|
+
const toProcess = id == null ? [...this.profiledRequestsMap.keys()] : [id]
|
|
33
|
+
|
|
34
|
+
for (const id of toProcess) {
|
|
35
|
+
const profilingInfo = this.profiledRequestsMap.get(id)
|
|
36
|
+
if (profilingInfo) {
|
|
37
|
+
const batch = profilingInfo.batch
|
|
38
|
+
profilingInfo.batch = []
|
|
39
|
+
await this.reporter.executeMainAction('profile', batch, profilingInfo.req).catch((e) => this.reporter.logger.error(e, profilingInfo.req))
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
40
44
|
emit (m, req, res) {
|
|
41
45
|
m.timestamp = m.timestamp || new Date().getTime()
|
|
42
46
|
|
package/lib/worker/reporter.js
CHANGED
|
@@ -143,6 +143,11 @@ class WorkerReporter extends Reporter {
|
|
|
143
143
|
currentPath,
|
|
144
144
|
errorLineNumberOffset
|
|
145
145
|
}, req) {
|
|
146
|
+
// we flush before running code in sandbox because it can potentially
|
|
147
|
+
// include code that blocks the whole process (like `while (true) {}`) and we
|
|
148
|
+
// want to ensure that the batched messages are flushed before trying to execute the code
|
|
149
|
+
await this.profiler.flush(req.context.rootId)
|
|
150
|
+
|
|
146
151
|
return this._runInSandbox({
|
|
147
152
|
manager,
|
|
148
153
|
context,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const LRU = require('lru-cache')
|
|
2
|
-
const
|
|
2
|
+
const stackTrace = require('stack-trace')
|
|
3
3
|
const { customAlphabet } = require('nanoid')
|
|
4
|
+
const safeSandbox = require('./safeSandbox')
|
|
4
5
|
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10)
|
|
5
6
|
|
|
6
7
|
module.exports = (reporter) => {
|
|
@@ -27,7 +28,7 @@ module.exports = (reporter) => {
|
|
|
27
28
|
context.__topLevelFunctions = {}
|
|
28
29
|
context.__handleError = (err) => handleError(reporter, err)
|
|
29
30
|
|
|
30
|
-
const { run, restore, contextifyValue, decontextifyValue, unproxyValue, sandbox, safeRequire } = safeSandbox(context, {
|
|
31
|
+
const { sourceFilesInfo, run, restore, contextifyValue, decontextifyValue, unproxyValue, sandbox, safeRequire } = safeSandbox(context, {
|
|
31
32
|
onLog: (log) => {
|
|
32
33
|
reporter.logger[log.level](log.message, { ...req, timestamp: log.timestamp })
|
|
33
34
|
},
|
|
@@ -61,7 +62,51 @@ module.exports = (reporter) => {
|
|
|
61
62
|
})
|
|
62
63
|
|
|
63
64
|
jsreportProxy = reporter.createProxy({ req, runInSandbox: run, context: sandbox, getTopLevelFunctions, safeRequire })
|
|
64
|
-
|
|
65
|
+
|
|
66
|
+
jsreportProxy.currentPath = async () => {
|
|
67
|
+
// we get the current path by throwing an error, which give us a stack trace
|
|
68
|
+
// which we analyze and see if some source file is associated to an entity
|
|
69
|
+
// if it is then we can properly get the path associated to it, if not we
|
|
70
|
+
// fallback to the current path passed as options
|
|
71
|
+
const filesCount = sourceFilesInfo.size
|
|
72
|
+
let resolvedPath = currentPath
|
|
73
|
+
|
|
74
|
+
if (filesCount > 0) {
|
|
75
|
+
const err = new Error('get me stack trace please')
|
|
76
|
+
const trace = stackTrace.parse(err)
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < trace.length; i++) {
|
|
79
|
+
const current = trace[i]
|
|
80
|
+
|
|
81
|
+
if (sourceFilesInfo.has(current.getFileName())) {
|
|
82
|
+
const { entity, entitySet } = sourceFilesInfo.get(current.getFileName())
|
|
83
|
+
|
|
84
|
+
if (entity != null && entitySet != null) {
|
|
85
|
+
resolvedPath = await reporter.folders.resolveEntityPath(entity, entitySet, req)
|
|
86
|
+
break
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return resolvedPath
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
jsreportProxy.currentDirectoryPath = async () => {
|
|
96
|
+
const currentPath = await jsreportProxy.currentPath()
|
|
97
|
+
|
|
98
|
+
if (currentPath != null) {
|
|
99
|
+
const localPath = currentPath.substring(0, currentPath.lastIndexOf('/'))
|
|
100
|
+
|
|
101
|
+
if (localPath === '') {
|
|
102
|
+
return '/'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return localPath
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return currentPath
|
|
109
|
+
}
|
|
65
110
|
|
|
66
111
|
// NOTE: it is important that cleanup, restore methods are not called from a function attached to the
|
|
67
112
|
// sandbox, because the arguments and return value of such function call will be sandboxed again, to solve this
|
|
@@ -150,6 +150,7 @@ module.exports = (_sandbox, options = {}) => {
|
|
|
150
150
|
return {
|
|
151
151
|
sandbox: vm._context,
|
|
152
152
|
console: _console,
|
|
153
|
+
sourceFilesInfo,
|
|
153
154
|
contextifyValue: (value) => {
|
|
154
155
|
return vm._internal.Contextify.value(value)
|
|
155
156
|
},
|
|
@@ -163,11 +164,11 @@ module.exports = (_sandbox, options = {}) => {
|
|
|
163
164
|
return getOriginalFromProxy(proxiesInVM, customProxies, value)
|
|
164
165
|
},
|
|
165
166
|
safeRequire: (modulePath) => _require(modulePath, { context: _sandbox, allowAllModules: true }),
|
|
166
|
-
run: async (code, { filename, errorLineNumberOffset = 0, source, entity } = {}) => {
|
|
167
|
+
run: async (code, { filename, errorLineNumberOffset = 0, source, entity, entitySet } = {}) => {
|
|
167
168
|
const script = new VMScript(code, filename)
|
|
168
169
|
|
|
169
170
|
if (filename != null && source != null) {
|
|
170
|
-
sourceFilesInfo.set(filename, { filename, source, entity, errorLineNumberOffset })
|
|
171
|
+
sourceFilesInfo.set(filename, { filename, source, entity, entitySet, errorLineNumberOffset })
|
|
171
172
|
}
|
|
172
173
|
|
|
173
174
|
// NOTE: if we need to upgrade vm2 we will need to check the source of this function
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsreport/jsreport-core",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "javascript based business reporting",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"report",
|
|
@@ -63,7 +63,6 @@
|
|
|
63
63
|
"triple-beam": "1.3.0",
|
|
64
64
|
"unset-value": "1.0.0",
|
|
65
65
|
"uuid": "8.3.2",
|
|
66
|
-
"v8-compile-cache": "*",
|
|
67
66
|
"vm2": "3.9.5",
|
|
68
67
|
"winston": "3.3.3",
|
|
69
68
|
"winston-transport": "4.4.0"
|