@nocobase/plugin-workflow-javascript 3.0.0-alpha.6 → 3.0.0-alpha.8
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/dist/client/index.js +1 -1
- package/dist/client-v2/index.js +1 -1
- package/dist/common/constants.d.ts +9 -0
- package/dist/common/constants.js +36 -0
- package/dist/externalVersion.js +6 -5
- package/dist/node_modules/joi/package.json +1 -1
- package/dist/node_modules/winston-transport/package.json +1 -1
- package/dist/server/RunningJobs.d.ts +34 -0
- package/dist/server/RunningJobs.js +105 -0
- package/dist/server/ScriptInstruction.d.ts +9 -21
- package/dist/server/ScriptInstruction.js +44 -134
- package/dist/server/ScriptWorkerRunner.d.ts +27 -0
- package/dist/server/ScriptWorkerRunner.js +140 -0
- package/dist/server/TaskConsumer.d.ts +29 -0
- package/dist/server/TaskConsumer.js +317 -0
- package/dist/server/TaskRecovery.d.ts +22 -0
- package/dist/server/TaskRecovery.js +133 -0
- package/dist/server/constants.d.ts +9 -0
- package/dist/server/constants.js +36 -0
- package/dist/server/plugin.d.ts +14 -0
- package/dist/server/plugin.js +92 -2
- package/package.json +2 -2
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
var __defProp = Object.defineProperty;
|
|
11
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
12
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __export = (target, all) => {
|
|
15
|
+
for (var name in all)
|
|
16
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
17
|
+
};
|
|
18
|
+
var __copyProps = (to, from, except, desc) => {
|
|
19
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
20
|
+
for (let key of __getOwnPropNames(from))
|
|
21
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
22
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
23
|
+
}
|
|
24
|
+
return to;
|
|
25
|
+
};
|
|
26
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
27
|
+
var RunningJobs_exports = {};
|
|
28
|
+
__export(RunningJobs_exports, {
|
|
29
|
+
ABORT_JAVASCRIPT_JOB_SYNC_MESSAGE_TYPE: () => ABORT_JAVASCRIPT_JOB_SYNC_MESSAGE_TYPE,
|
|
30
|
+
JAVASCRIPT_SYNC_MESSAGE_CHANNEL: () => JAVASCRIPT_SYNC_MESSAGE_CHANNEL,
|
|
31
|
+
RunningJobs: () => RunningJobs,
|
|
32
|
+
getAbortReason: () => getAbortReason
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(RunningJobs_exports);
|
|
35
|
+
const JAVASCRIPT_SYNC_MESSAGE_CHANNEL = "@nocobase/plugin-workflow-javascript";
|
|
36
|
+
const ABORT_JAVASCRIPT_JOB_SYNC_MESSAGE_TYPE = "abortJavaScriptJob";
|
|
37
|
+
function getAbortReason(reason) {
|
|
38
|
+
if (reason instanceof Error) {
|
|
39
|
+
return reason.message;
|
|
40
|
+
}
|
|
41
|
+
if (typeof reason === "string") {
|
|
42
|
+
return reason;
|
|
43
|
+
}
|
|
44
|
+
return void 0;
|
|
45
|
+
}
|
|
46
|
+
class RunningJobs {
|
|
47
|
+
jobs = /* @__PURE__ */ new Map();
|
|
48
|
+
executionJobs = /* @__PURE__ */ new Map();
|
|
49
|
+
register(job) {
|
|
50
|
+
const jobId = String(job.jobId);
|
|
51
|
+
const executionId = String(job.executionId);
|
|
52
|
+
const executionJobs = this.executionJobs.get(executionId) ?? /* @__PURE__ */ new Set();
|
|
53
|
+
this.jobs.set(jobId, job);
|
|
54
|
+
executionJobs.add(jobId);
|
|
55
|
+
this.executionJobs.set(executionId, executionJobs);
|
|
56
|
+
return () => this.unregister(job.jobId);
|
|
57
|
+
}
|
|
58
|
+
unregister(jobId) {
|
|
59
|
+
const key = String(jobId);
|
|
60
|
+
const job = this.jobs.get(key);
|
|
61
|
+
if (!job) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.jobs.delete(key);
|
|
65
|
+
const executionId = String(job.executionId);
|
|
66
|
+
const executionJobs = this.executionJobs.get(executionId);
|
|
67
|
+
executionJobs == null ? void 0 : executionJobs.delete(key);
|
|
68
|
+
if (!(executionJobs == null ? void 0 : executionJobs.size)) {
|
|
69
|
+
this.executionJobs.delete(executionId);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
abortJob(jobId, reason) {
|
|
73
|
+
const job = this.jobs.get(String(jobId));
|
|
74
|
+
if (!job) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
job.abort(reason);
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
abortExecution(executionId, reason) {
|
|
81
|
+
var _a;
|
|
82
|
+
const jobIds = this.executionJobs.get(String(executionId));
|
|
83
|
+
if (!(jobIds == null ? void 0 : jobIds.size)) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
for (const jobId of jobIds) {
|
|
87
|
+
(_a = this.jobs.get(jobId)) == null ? void 0 : _a.abort(reason);
|
|
88
|
+
}
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
async abortAcrossInstances(workflowPlugin, message) {
|
|
92
|
+
if (this.abortJob(message.jobId, message.reason)) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
await workflowPlugin.app.syncMessageManager.publish(JAVASCRIPT_SYNC_MESSAGE_CHANNEL, message);
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
100
|
+
0 && (module.exports = {
|
|
101
|
+
ABORT_JAVASCRIPT_JOB_SYNC_MESSAGE_TYPE,
|
|
102
|
+
JAVASCRIPT_SYNC_MESSAGE_CHANNEL,
|
|
103
|
+
RunningJobs,
|
|
104
|
+
getAbortReason
|
|
105
|
+
});
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { Logger } from 'winston';
|
|
10
10
|
import Joi from 'joi';
|
|
11
|
-
import { Processor, Instruction, FlowNodeModel } from '@nocobase/plugin-workflow';
|
|
11
|
+
import WorkflowPlugin, { Processor, Instruction, FlowNodeModel } from '@nocobase/plugin-workflow';
|
|
12
|
+
import { RunningJobs } from './RunningJobs';
|
|
13
|
+
import { ScriptArguments, ScriptRunResult } from './ScriptWorkerRunner';
|
|
12
14
|
type ScriptArgument = {
|
|
13
15
|
name: string;
|
|
14
16
|
value?: unknown;
|
|
@@ -19,41 +21,27 @@ type ScriptConfig = {
|
|
|
19
21
|
continue?: boolean;
|
|
20
22
|
arguments?: ScriptArgument[];
|
|
21
23
|
};
|
|
22
|
-
type ScriptArguments = Record<string, unknown> | unknown[];
|
|
23
24
|
export default class ScriptInstruction extends Instruction {
|
|
24
|
-
|
|
25
|
-
* Returns the worker script path based on whether WORKFLOW_SCRIPT_MODULES is configured.
|
|
26
|
-
* - WORKFLOW_SCRIPT_MODULES set: uses Node.js vm with require support (unsafe; not a security boundary)
|
|
27
|
-
* - WORKFLOW_SCRIPT_MODULES unset: uses QuickJS (WASM) for maximum security (no require, no Node.js APIs)
|
|
28
|
-
*/
|
|
25
|
+
private readonly runningJobs;
|
|
29
26
|
static get workerScript(): string;
|
|
30
27
|
static run(source: string, args: ScriptArguments, options: {
|
|
31
28
|
logger: Logger;
|
|
32
29
|
timeout?: number;
|
|
33
30
|
signal?: AbortSignal;
|
|
34
|
-
}): Promise<
|
|
35
|
-
|
|
36
|
-
result: string;
|
|
37
|
-
} | {
|
|
38
|
-
status: 1;
|
|
39
|
-
result: unknown;
|
|
40
|
-
}>;
|
|
31
|
+
}): Promise<ScriptRunResult>;
|
|
32
|
+
constructor(workflow: WorkflowPlugin, runningJobs: RunningJobs);
|
|
41
33
|
configSchema: Joi.ObjectSchema<any>;
|
|
42
34
|
run(node: FlowNodeModel, prevJob: any, processor: Processor, options?: {
|
|
43
35
|
signal?: AbortSignal;
|
|
44
36
|
}): Promise<{
|
|
45
37
|
result: unknown;
|
|
46
|
-
status:
|
|
38
|
+
status: 1 | -2;
|
|
47
39
|
}>;
|
|
48
40
|
resume(node: FlowNodeModel, job: any, processor: Processor): Promise<any>;
|
|
49
41
|
test(config?: ScriptConfig): Promise<{
|
|
50
42
|
log: string;
|
|
51
|
-
status:
|
|
52
|
-
result
|
|
53
|
-
} | {
|
|
54
|
-
log: string;
|
|
55
|
-
status: 1;
|
|
56
|
-
result: unknown;
|
|
43
|
+
status: number;
|
|
44
|
+
result?: unknown;
|
|
57
45
|
}>;
|
|
58
46
|
}
|
|
59
47
|
export {};
|
|
@@ -39,102 +39,23 @@ __export(ScriptInstruction_exports, {
|
|
|
39
39
|
default: () => ScriptInstruction
|
|
40
40
|
});
|
|
41
41
|
module.exports = __toCommonJS(ScriptInstruction_exports);
|
|
42
|
-
var import_node_events = require("node:events");
|
|
43
|
-
var import_node_path = __toESM(require("node:path"));
|
|
44
|
-
var import_node_worker_threads = require("node:worker_threads");
|
|
45
42
|
var import_winston = __toESM(require("winston"));
|
|
46
43
|
var import_joi = __toESM(require("joi"));
|
|
47
44
|
var import_plugin_workflow = require("@nocobase/plugin-workflow");
|
|
48
45
|
var import_cache_logger = require("./cache-logger");
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
var import_constants = require("./constants");
|
|
47
|
+
var import_RunningJobs = require("./RunningJobs");
|
|
48
|
+
var import_ScriptWorkerRunner = require("./ScriptWorkerRunner");
|
|
52
49
|
class ScriptInstruction extends import_plugin_workflow.Instruction {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
*/
|
|
50
|
+
constructor(workflow, runningJobs) {
|
|
51
|
+
super(workflow);
|
|
52
|
+
this.runningJobs = runningJobs;
|
|
53
|
+
}
|
|
58
54
|
static get workerScript() {
|
|
59
|
-
|
|
60
|
-
return import_node_path.default.join(__dirname, hasModules ? "Vm.js" : "QuickJs.js");
|
|
55
|
+
return import_ScriptWorkerRunner.ScriptWorkerRunner.workerScript;
|
|
61
56
|
}
|
|
62
57
|
static async run(source, args, options) {
|
|
63
|
-
|
|
64
|
-
const worker = new import_node_worker_threads.Worker(this.workerScript, {
|
|
65
|
-
workerData: { source, args, options: timeout ? { timeout } : {} }
|
|
66
|
-
});
|
|
67
|
-
worker.stdout.on("data", (data) => {
|
|
68
|
-
logger.info(data.toString());
|
|
69
|
-
});
|
|
70
|
-
worker.stderr.on("data", (data) => {
|
|
71
|
-
logger.error(data.toString());
|
|
72
|
-
});
|
|
73
|
-
const outputClosed = Promise.all([(0, import_node_events.once)(worker.stdout, "close"), (0, import_node_events.once)(worker.stderr, "close")]);
|
|
74
|
-
const outcomePromise = new Promise((resolve) => {
|
|
75
|
-
const finish = (outcome2) => {
|
|
76
|
-
worker.removeListener("message", messageListener);
|
|
77
|
-
worker.removeListener("error", errorListener);
|
|
78
|
-
worker.removeListener("exit", exitListener);
|
|
79
|
-
signal == null ? void 0 : signal.removeEventListener("abort", abortListener);
|
|
80
|
-
resolve(outcome2);
|
|
81
|
-
};
|
|
82
|
-
const messageListener = (message) => {
|
|
83
|
-
if (isWorkerResult(message)) {
|
|
84
|
-
finish({ type: "result", result: message.result });
|
|
85
|
-
}
|
|
86
|
-
};
|
|
87
|
-
const errorListener = (error) => {
|
|
88
|
-
finish({ type: "error", error });
|
|
89
|
-
};
|
|
90
|
-
const exitListener = (code) => {
|
|
91
|
-
finish({ type: "exit", code });
|
|
92
|
-
};
|
|
93
|
-
const abortListener = () => {
|
|
94
|
-
finish({ type: "aborted" });
|
|
95
|
-
};
|
|
96
|
-
worker.on("message", messageListener);
|
|
97
|
-
worker.once("error", errorListener);
|
|
98
|
-
worker.once("exit", exitListener);
|
|
99
|
-
signal == null ? void 0 : signal.addEventListener("abort", abortListener, { once: true });
|
|
100
|
-
if (signal == null ? void 0 : signal.aborted) {
|
|
101
|
-
abortListener();
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
const outcome = await outcomePromise;
|
|
105
|
-
try {
|
|
106
|
-
if (outcome.type !== "exit") {
|
|
107
|
-
await worker.terminate();
|
|
108
|
-
}
|
|
109
|
-
await outputClosed;
|
|
110
|
-
} catch (e) {
|
|
111
|
-
if (outcome.type === "aborted" || (signal == null ? void 0 : signal.aborted)) {
|
|
112
|
-
throw signal.reason instanceof Error ? signal.reason : new import_plugin_workflow.WorkflowTimeoutError();
|
|
113
|
-
}
|
|
114
|
-
return {
|
|
115
|
-
status: import_plugin_workflow.JOB_STATUS.ERROR,
|
|
116
|
-
result: e instanceof Error ? e.message : String(e)
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
if (outcome.type === "aborted") {
|
|
120
|
-
throw (signal == null ? void 0 : signal.reason) instanceof Error ? signal.reason : new import_plugin_workflow.WorkflowTimeoutError();
|
|
121
|
-
}
|
|
122
|
-
if (outcome.type === "error") {
|
|
123
|
-
return {
|
|
124
|
-
status: import_plugin_workflow.JOB_STATUS.ERROR,
|
|
125
|
-
result: outcome.error.message
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
if (outcome.type === "exit" && outcome.code !== 0) {
|
|
129
|
-
return {
|
|
130
|
-
status: import_plugin_workflow.JOB_STATUS.ERROR,
|
|
131
|
-
result: `Worker stopped with exit code ${outcome.code}`
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
return {
|
|
135
|
-
status: import_plugin_workflow.JOB_STATUS.RESOLVED,
|
|
136
|
-
result: outcome.type === "result" ? outcome.result : void 0
|
|
137
|
-
};
|
|
58
|
+
return import_ScriptWorkerRunner.ScriptWorkerRunner.run(source, args, options);
|
|
138
59
|
}
|
|
139
60
|
configSchema = import_joi.default.object({
|
|
140
61
|
content: import_joi.default.string(),
|
|
@@ -148,6 +69,7 @@ class ScriptInstruction extends import_plugin_workflow.Instruction {
|
|
|
148
69
|
).optional()
|
|
149
70
|
});
|
|
150
71
|
async run(node, prevJob, processor, options) {
|
|
72
|
+
var _a, _b;
|
|
151
73
|
const { content = "", continue: cont, timeout } = node.config;
|
|
152
74
|
const args = processor.getParsedValue(node.config.arguments ?? [], node.id);
|
|
153
75
|
const _args = args.reduce((pre, item) => ({ ...pre, [item.name]: item.value }), {});
|
|
@@ -169,60 +91,48 @@ class ScriptInstruction extends import_plugin_workflow.Instruction {
|
|
|
169
91
|
status: cont ? import_plugin_workflow.JOB_STATUS.RESOLVED : result.status === import_plugin_workflow.JOB_STATUS.RESOLVED ? import_plugin_workflow.JOB_STATUS.RESOLVED : import_plugin_workflow.JOB_STATUS.ERROR
|
|
170
92
|
};
|
|
171
93
|
}
|
|
94
|
+
const meta = {
|
|
95
|
+
args: _args
|
|
96
|
+
};
|
|
172
97
|
const { id } = processor.saveJob({
|
|
173
98
|
status: import_plugin_workflow.JOB_STATUS.PENDING,
|
|
174
99
|
nodeId: node.id,
|
|
175
100
|
nodeKey: node.key,
|
|
176
|
-
upstreamId: (prevJob == null ? void 0 : prevJob.id) ?? null
|
|
101
|
+
upstreamId: (prevJob == null ? void 0 : prevJob.id) ?? null,
|
|
102
|
+
startedAt: null,
|
|
103
|
+
meta
|
|
177
104
|
});
|
|
178
|
-
|
|
179
|
-
|
|
105
|
+
const abortQueuedJob = async () => {
|
|
106
|
+
var _a2;
|
|
107
|
+
await this.runningJobs.abortAcrossInstances(this.workflow, {
|
|
108
|
+
type: import_RunningJobs.ABORT_JAVASCRIPT_JOB_SYNC_MESSAGE_TYPE,
|
|
109
|
+
jobId: id,
|
|
110
|
+
executionId: processor.execution.id,
|
|
111
|
+
reason: (0, import_RunningJobs.getAbortReason)((_a2 = options == null ? void 0 : options.signal) == null ? void 0 : _a2.reason)
|
|
112
|
+
});
|
|
113
|
+
};
|
|
114
|
+
if ((_a = options == null ? void 0 : options.signal) == null ? void 0 : _a.aborted) {
|
|
115
|
+
await abortQueuedJob();
|
|
116
|
+
} else {
|
|
117
|
+
(_b = options == null ? void 0 : options.signal) == null ? void 0 : _b.addEventListener(
|
|
118
|
+
"abort",
|
|
119
|
+
() => {
|
|
120
|
+
abortQueuedJob().catch((error) => {
|
|
121
|
+
processor.logger.error(`broadcasting JavaScript job (${id}) abort signal failed`, { error });
|
|
122
|
+
});
|
|
123
|
+
},
|
|
124
|
+
{ once: true }
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
processor.logger.info(`script (#${node.id}) has been queued, waiting for JavaScript Worker resource...`);
|
|
128
|
+
await processor.exit();
|
|
180
129
|
try {
|
|
181
|
-
await
|
|
130
|
+
await this.workflow.app.eventQueue.publish(import_constants.PENDING_JAVASCRIPT_TASK_CHANNEL, {
|
|
131
|
+
jobId: id
|
|
132
|
+
});
|
|
182
133
|
} catch (error) {
|
|
183
|
-
|
|
184
|
-
throw error;
|
|
134
|
+
processor.logger.error(`publishing JavaScript job (${id}) failed, recovery will republish it`, { error });
|
|
185
135
|
}
|
|
186
|
-
const jobResult = {
|
|
187
|
-
status: import_plugin_workflow.JOB_STATUS.PENDING
|
|
188
|
-
};
|
|
189
|
-
this.constructor.run(content, _args, { timeout, logger: processor.logger, signal: abortHandle.signal }).then((res) => {
|
|
190
|
-
if (res.status === import_plugin_workflow.JOB_STATUS.RESOLVED) {
|
|
191
|
-
processor.logger.info(`script (#${node.id}) get result success`);
|
|
192
|
-
jobResult.status = import_plugin_workflow.JOB_STATUS.RESOLVED;
|
|
193
|
-
jobResult.result = res.result;
|
|
194
|
-
processor.logger.info(`run script execution success, node id: ${node.id},the result is ${res.result}`);
|
|
195
|
-
return;
|
|
196
|
-
}
|
|
197
|
-
if (cont) {
|
|
198
|
-
processor.logger.warn(`script (#${node.id}) get result failed, the reason is ${res.result}`);
|
|
199
|
-
jobResult.status = import_plugin_workflow.JOB_STATUS.RESOLVED;
|
|
200
|
-
jobResult.result = res.result;
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
processor.logger.info(`script (#${node.id}) get result failed, the reason is ${res.result}`);
|
|
204
|
-
jobResult.status = import_plugin_workflow.JOB_STATUS.ERROR;
|
|
205
|
-
jobResult.result = res.result;
|
|
206
|
-
}).catch((e) => {
|
|
207
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
208
|
-
processor.logger.error(`script (#${node.id}) get result failed, the reason is ${message}`);
|
|
209
|
-
jobResult.status = import_plugin_workflow.JOB_STATUS.ERROR;
|
|
210
|
-
jobResult.result = message;
|
|
211
|
-
}).finally(() => {
|
|
212
|
-
abortHandle.dispose();
|
|
213
|
-
processor.logger.debug(`script (#${node.id}) ended, resume workflow...`);
|
|
214
|
-
setImmediate(async () => {
|
|
215
|
-
const job = await this.workflow.db.getRepository("jobs").findOne({ filterByTk: id });
|
|
216
|
-
const execution = await job.getExecution();
|
|
217
|
-
if (execution.status !== 0) {
|
|
218
|
-
processor.logger.warn(`script (#${node.id}) result discarded because execution (${execution.id}) is ended`);
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
job.set(jobResult);
|
|
222
|
-
await this.workflow.resume(job).catch(() => {
|
|
223
|
-
});
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
136
|
}
|
|
227
137
|
async resume(node, job, processor) {
|
|
228
138
|
return job;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
import { Logger } from 'winston';
|
|
10
|
+
export type ScriptArguments = Record<string, unknown> | unknown[] | null;
|
|
11
|
+
export type ScriptRunResult = {
|
|
12
|
+
status: number;
|
|
13
|
+
result?: unknown;
|
|
14
|
+
};
|
|
15
|
+
export declare class ScriptWorkerRunner {
|
|
16
|
+
/**
|
|
17
|
+
* Returns the worker script path based on whether WORKFLOW_SCRIPT_MODULES is configured.
|
|
18
|
+
* - WORKFLOW_SCRIPT_MODULES set: uses Node.js vm with require support (unsafe; not a security boundary)
|
|
19
|
+
* - WORKFLOW_SCRIPT_MODULES unset: uses QuickJS (WASM) for maximum security (no require, no Node.js APIs)
|
|
20
|
+
*/
|
|
21
|
+
static get workerScript(): string;
|
|
22
|
+
static run(source: string, args: ScriptArguments, options: {
|
|
23
|
+
logger: Logger;
|
|
24
|
+
timeout?: number;
|
|
25
|
+
signal?: AbortSignal;
|
|
26
|
+
}): Promise<ScriptRunResult>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
var __create = Object.create;
|
|
11
|
+
var __defProp = Object.defineProperty;
|
|
12
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
13
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
14
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
15
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
16
|
+
var __export = (target, all) => {
|
|
17
|
+
for (var name in all)
|
|
18
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
19
|
+
};
|
|
20
|
+
var __copyProps = (to, from, except, desc) => {
|
|
21
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
22
|
+
for (let key of __getOwnPropNames(from))
|
|
23
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
24
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
25
|
+
}
|
|
26
|
+
return to;
|
|
27
|
+
};
|
|
28
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
29
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
30
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
31
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
32
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
33
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
34
|
+
mod
|
|
35
|
+
));
|
|
36
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
37
|
+
var ScriptWorkerRunner_exports = {};
|
|
38
|
+
__export(ScriptWorkerRunner_exports, {
|
|
39
|
+
ScriptWorkerRunner: () => ScriptWorkerRunner
|
|
40
|
+
});
|
|
41
|
+
module.exports = __toCommonJS(ScriptWorkerRunner_exports);
|
|
42
|
+
var import_node_events = require("node:events");
|
|
43
|
+
var import_node_path = __toESM(require("node:path"));
|
|
44
|
+
var import_node_worker_threads = require("node:worker_threads");
|
|
45
|
+
var import_plugin_workflow = require("@nocobase/plugin-workflow");
|
|
46
|
+
function isWorkerResult(message) {
|
|
47
|
+
return typeof message === "object" && message !== null && "type" in message && message.type === "result";
|
|
48
|
+
}
|
|
49
|
+
class ScriptWorkerRunner {
|
|
50
|
+
/**
|
|
51
|
+
* Returns the worker script path based on whether WORKFLOW_SCRIPT_MODULES is configured.
|
|
52
|
+
* - WORKFLOW_SCRIPT_MODULES set: uses Node.js vm with require support (unsafe; not a security boundary)
|
|
53
|
+
* - WORKFLOW_SCRIPT_MODULES unset: uses QuickJS (WASM) for maximum security (no require, no Node.js APIs)
|
|
54
|
+
*/
|
|
55
|
+
static get workerScript() {
|
|
56
|
+
const hasModules = (process.env.WORKFLOW_SCRIPT_MODULES ?? "").split(",").filter(Boolean).length > 0;
|
|
57
|
+
return import_node_path.default.join(__dirname, hasModules ? "Vm.js" : "QuickJs.js");
|
|
58
|
+
}
|
|
59
|
+
static async run(source, args, options) {
|
|
60
|
+
const { logger, timeout, signal } = options;
|
|
61
|
+
const worker = new import_node_worker_threads.Worker(this.workerScript, {
|
|
62
|
+
workerData: { source, args, options: timeout ? { timeout } : {} }
|
|
63
|
+
});
|
|
64
|
+
worker.stdout.on("data", (data) => {
|
|
65
|
+
logger.info(data.toString());
|
|
66
|
+
});
|
|
67
|
+
worker.stderr.on("data", (data) => {
|
|
68
|
+
logger.error(data.toString());
|
|
69
|
+
});
|
|
70
|
+
const outputClosed = Promise.all([(0, import_node_events.once)(worker.stdout, "close"), (0, import_node_events.once)(worker.stderr, "close")]);
|
|
71
|
+
const outcomePromise = new Promise((resolve) => {
|
|
72
|
+
const finish = (outcome2) => {
|
|
73
|
+
worker.removeListener("message", messageListener);
|
|
74
|
+
worker.removeListener("error", errorListener);
|
|
75
|
+
worker.removeListener("exit", exitListener);
|
|
76
|
+
signal == null ? void 0 : signal.removeEventListener("abort", abortListener);
|
|
77
|
+
resolve(outcome2);
|
|
78
|
+
};
|
|
79
|
+
const messageListener = (message) => {
|
|
80
|
+
if (isWorkerResult(message)) {
|
|
81
|
+
finish({ type: "result", result: message.result });
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const errorListener = (error) => {
|
|
85
|
+
finish({ type: "error", error });
|
|
86
|
+
};
|
|
87
|
+
const exitListener = (code) => {
|
|
88
|
+
finish({ type: "exit", code });
|
|
89
|
+
};
|
|
90
|
+
const abortListener = () => {
|
|
91
|
+
finish({ type: "aborted" });
|
|
92
|
+
};
|
|
93
|
+
worker.on("message", messageListener);
|
|
94
|
+
worker.once("error", errorListener);
|
|
95
|
+
worker.once("exit", exitListener);
|
|
96
|
+
signal == null ? void 0 : signal.addEventListener("abort", abortListener, { once: true });
|
|
97
|
+
if (signal == null ? void 0 : signal.aborted) {
|
|
98
|
+
abortListener();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
const outcome = await outcomePromise;
|
|
102
|
+
try {
|
|
103
|
+
if (outcome.type !== "exit") {
|
|
104
|
+
await worker.terminate();
|
|
105
|
+
}
|
|
106
|
+
await outputClosed;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (outcome.type === "aborted" || (signal == null ? void 0 : signal.aborted)) {
|
|
109
|
+
throw signal.reason instanceof Error ? signal.reason : new import_plugin_workflow.WorkflowTimeoutError();
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
status: import_plugin_workflow.JOB_STATUS.ERROR,
|
|
113
|
+
result: error instanceof Error ? error.message : String(error)
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (outcome.type === "aborted") {
|
|
117
|
+
throw (signal == null ? void 0 : signal.reason) instanceof Error ? signal.reason : new import_plugin_workflow.WorkflowTimeoutError();
|
|
118
|
+
}
|
|
119
|
+
if (outcome.type === "error") {
|
|
120
|
+
return {
|
|
121
|
+
status: import_plugin_workflow.JOB_STATUS.ERROR,
|
|
122
|
+
result: outcome.error.message
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
if (outcome.type === "exit" && outcome.code !== 0) {
|
|
126
|
+
return {
|
|
127
|
+
status: import_plugin_workflow.JOB_STATUS.ERROR,
|
|
128
|
+
result: `Worker stopped with exit code ${outcome.code}`
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
status: import_plugin_workflow.JOB_STATUS.RESOLVED,
|
|
133
|
+
result: outcome.type === "result" ? outcome.result : void 0
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
138
|
+
0 && (module.exports = {
|
|
139
|
+
ScriptWorkerRunner
|
|
140
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
import type WorkflowPlugin from '@nocobase/plugin-workflow';
|
|
10
|
+
import type { QueueEventOptions } from '@nocobase/server';
|
|
11
|
+
import { RunningJobs } from './RunningJobs';
|
|
12
|
+
export declare class TaskConsumer {
|
|
13
|
+
private readonly workflowPlugin;
|
|
14
|
+
private readonly runningJobs;
|
|
15
|
+
private ready;
|
|
16
|
+
private closing;
|
|
17
|
+
private readonly processing;
|
|
18
|
+
constructor(workflowPlugin: WorkflowPlugin, runningJobs: RunningJobs);
|
|
19
|
+
setReady(ready: boolean): void;
|
|
20
|
+
idle(): boolean;
|
|
21
|
+
beforeStop(): Promise<void>;
|
|
22
|
+
readonly process: QueueEventOptions['process'];
|
|
23
|
+
private processWithPermit;
|
|
24
|
+
private claimJob;
|
|
25
|
+
private finishClaimedJob;
|
|
26
|
+
private settleClaimedJob;
|
|
27
|
+
private updatePendingJob;
|
|
28
|
+
private startClaimHeartbeat;
|
|
29
|
+
}
|