verteilen-core 1.0.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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2 -0
  3. package/bun.lock +730 -0
  4. package/index.ts +1 -0
  5. package/jest.config.js +8 -0
  6. package/package.json +42 -0
  7. package/src/client/analysis.ts +377 -0
  8. package/src/client/client.ts +230 -0
  9. package/src/client/cluster.ts +125 -0
  10. package/src/client/execute.ts +210 -0
  11. package/src/client/http.ts +45 -0
  12. package/src/client/javascript.ts +535 -0
  13. package/src/client/job_execute.ts +216 -0
  14. package/src/client/job_parameter.ts +41 -0
  15. package/src/client/os.ts +210 -0
  16. package/src/client/parameter.ts +58 -0
  17. package/src/client/resource.ts +121 -0
  18. package/src/client/shell.ts +147 -0
  19. package/src/interface/base.ts +82 -0
  20. package/src/interface/bus.ts +144 -0
  21. package/src/interface/enum.ts +181 -0
  22. package/src/interface/execute.ts +47 -0
  23. package/src/interface/record.ts +131 -0
  24. package/src/interface/server.ts +91 -0
  25. package/src/interface/struct.ts +292 -0
  26. package/src/interface/table.ts +34 -0
  27. package/src/interface/ui.ts +35 -0
  28. package/src/interface.ts +50 -0
  29. package/src/lan/en.json +395 -0
  30. package/src/lan/zh_TW.json +395 -0
  31. package/src/plugins/i18n.ts +20 -0
  32. package/src/script/console_manager.ts +135 -0
  33. package/src/script/console_server_manager.ts +46 -0
  34. package/src/script/execute/base.ts +309 -0
  35. package/src/script/execute/feedback.ts +212 -0
  36. package/src/script/execute/region_job.ts +14 -0
  37. package/src/script/execute/region_project.ts +23 -0
  38. package/src/script/execute/region_subtask.ts +14 -0
  39. package/src/script/execute/region_task.ts +23 -0
  40. package/src/script/execute/runner.ts +339 -0
  41. package/src/script/execute/util_parser.ts +175 -0
  42. package/src/script/execute_manager.ts +348 -0
  43. package/src/script/socket_manager.ts +329 -0
  44. package/src/script/webhook_manager.ts +6 -0
  45. package/src/script/webhook_server_manager.ts +102 -0
  46. package/src/util/server/console_handle.ts +248 -0
  47. package/src/util/server/log_handle.ts +194 -0
  48. package/test/TEST.ts +63 -0
  49. package/test/client/execute.test.ts +54 -0
  50. package/test/client/javascript.test.ts +78 -0
  51. package/test/client/server.test.ts +26 -0
  52. package/test/client/task.test.ts +136 -0
  53. package/test/script/parser.test.ts +110 -0
  54. package/test/script/socket.test.ts +27 -0
  55. package/tsconfig.json +15 -0
@@ -0,0 +1,339 @@
1
+ // ========================
2
+ //
3
+ // Share Codebase
4
+ //
5
+ // ========================
6
+ import { v6 as uuidv6 } from 'uuid';
7
+ import { CronJobState, DataType, ExecuteState, Header, Job, Project, Task, WebsocketPack } from "../../interface";
8
+ import { ExecuteManager_Feedback } from "./feedback";
9
+ import { Util_Parser } from './util_parser';
10
+
11
+ /**
12
+ * The execute runner
13
+ */
14
+ export class ExecuteManager_Runner extends ExecuteManager_Feedback {
15
+ /**
16
+ * Execute project
17
+ */
18
+ protected ExecuteProject = (project:Project) => {
19
+ if(this.current_t == undefined && project.task.length > 0 && this.t_state != ExecuteState.FINISH){
20
+ // When we are just start it, the project run
21
+ this.current_t = project.task[0]
22
+ this.messager_log(`[Execute] Task Start ${this.current_t.uuid}`)
23
+ this.messager_log(`[Execute] Task cron state: ${this.current_t.cronjob}`)
24
+ this.current_job = []
25
+ this.current_cron = []
26
+ } else if (project.task.length == 0){
27
+ this.current_t = undefined
28
+ }
29
+
30
+ /**
31
+ * In any case, if the task has value, this mean we are in the task stage, so, just ignore everything.\
32
+ * Go for the task stage
33
+ */
34
+ if(this.current_t != undefined){
35
+ this.ExecuteTask(project, this.current_t)
36
+ }else{
37
+ /**
38
+ * If we are here, task is none by this case. This can only be
39
+ * * A: We are finish all the tasks, And there is no next project, So just mark as finish for entire process
40
+ * * B: We are finish all the tasks, Go to next project
41
+ */
42
+ const index = this.current_projects.findIndex(x => x.uuid == project.uuid)
43
+ if(index < this.current_projects.length - 1){
44
+ // * Case A: Next project
45
+ this.messager_log(`[Execute] Project Finish ${this.current_p!.uuid}`)
46
+ this.proxy?.executeProjectFinish([this.current_p!, index])
47
+ this.current_p = this.current_projects[index + 1]
48
+ this.proxy?.executeProjectStart([this.current_p!, index + 1])
49
+ this.t_state = ExecuteState.NONE
50
+ }else{
51
+ // * Case B: Finish entire thing
52
+ this.messager_log(`[Execute] Project Finish ${this.current_p!.uuid}`)
53
+ this.proxy?.executeProjectFinish([this.current_p!, index])
54
+ this.current_p = undefined
55
+ this.state = ExecuteState.FINISH
56
+ this.t_state = ExecuteState.NONE
57
+ }
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Execute task
63
+ */
64
+ private ExecuteTask = (project:Project, task:Task) => {
65
+ /**
66
+ * When it's the first iteration for this task
67
+ */
68
+ if(this.t_state == ExecuteState.NONE){
69
+ this.t_state = ExecuteState.RUNNING
70
+ this.current_multithread = task.multi ? this.get_task_multi_count(task) : 1
71
+ this.current_task_count = this.get_task_state_count(task)
72
+ }
73
+ let allJobFinish = false
74
+ const hasJob = task.jobs.length > 0
75
+
76
+ /**
77
+ * If a task has no job... we have to skip it...
78
+ */
79
+ if(!hasJob){
80
+ // We end it gracefully.
81
+ this.proxy?.executeTaskStart([task, this.current_task_count ])
82
+ this.proxy?.executeTaskFinish(task)
83
+ this.messager_log(`[Execute] Skip ! No job exists ${task.uuid}`)
84
+ this.ExecuteTask_AllFinish(project, task)
85
+ return
86
+ }
87
+
88
+ if(task.setupjob){
89
+ allJobFinish = this.ExecuteTask_Setup(project, task, this.current_task_count)
90
+ } else if (task.cronjob){
91
+ allJobFinish = this.ExecuteTask_Cronjob(project, task, this.current_task_count)
92
+ } else {
93
+ allJobFinish = this.ExecuteTask_Single(project, task, this.current_task_count)
94
+ }
95
+
96
+ if (allJobFinish){
97
+ this.ExecuteTask_AllFinish(project, task)
98
+ }
99
+ }
100
+
101
+ /**
102
+ * It will spawn amounts of cronjob and send the tasks for assigned node to execute them one by one
103
+ * @param taskCount Should be equal to cronjob result
104
+ * @returns Is finish executing
105
+ */
106
+ private ExecuteTask_Cronjob(project:Project, task:Task, taskCount:number):boolean {
107
+ let ns:Array<WebsocketPack> = this.get_idle_open()
108
+ let allJobFinish = false
109
+
110
+ /**
111
+ * if current_cron length is zero\
112
+ * this means the init process has not been run yet
113
+ */
114
+ if(this.current_cron.length == 0){
115
+ // First time
116
+ this.Init_CronContainer(task, taskCount)
117
+ this.messager_log(`[Execute] TaskCount: ${taskCount}`)
118
+ } else{
119
+ // If disconnect or deleted...
120
+ /**
121
+ * We query all the cron state and get all the processing first and count it\
122
+ * All we want is to filter out the node which is fully load\
123
+ * So we can follow the multithread limit to send the mission
124
+ */
125
+ const worker = this.current_cron.filter(x => x.uuid != '').map(x => x.uuid)
126
+ const counter:Array<[string, number]> = []
127
+ worker.forEach(uuid => {
128
+ const index = counter.findIndex(x => x[0] == uuid)
129
+ if(index == -1) counter.push([uuid, 1])
130
+ else counter[index][1] += 1
131
+ })
132
+ const fullLoadUUID = counter.filter(x => x[1] >= this.current_multithread).map(x => x[0])
133
+ ns = ns.filter(x => !fullLoadUUID.includes(x.uuid))
134
+ }
135
+
136
+ if(this.check_all_cron_end()){
137
+ allJobFinish = true
138
+ }else{
139
+ // Assign worker
140
+ // Find the cron which is need to be execute by a node
141
+ const needs = this.current_cron.filter(x => x.uuid == '' && x.work.filter(y => y.state != ExecuteState.FINISH && y.state != ExecuteState.ERROR).length > 0)
142
+ const min = Math.min(needs.length, ns.length)
143
+ for(let i = 0; i < min; i++){
144
+ needs[i].uuid = ns[i].uuid
145
+ }
146
+ const single = this.current_cron.filter(x => x.uuid != '')
147
+ // Execute
148
+ for(var cronwork of single){
149
+ const index = this.current_nodes.findIndex(x => x.uuid == cronwork.uuid)
150
+ if(index != -1){
151
+ this.ExecuteCronTask(project, task, cronwork, this.current_nodes[index])
152
+ }
153
+ }
154
+ }
155
+ return allJobFinish
156
+ }
157
+
158
+ /**
159
+ * There will be no CronTask be called, it will go straight to the Execute job section
160
+ * @param taskCount Must be 1
161
+ * @returns Is finish executing
162
+ */
163
+ private ExecuteTask_Single(project:Project, task:Task, taskCount:number):boolean {
164
+ let allJobFinish = false
165
+ let ns:Array<WebsocketPack> = []
166
+ if(this.current_job.length > 0){
167
+ // If disconnect or deleted...
168
+ const last = this.current_nodes.find(x => x.uuid == this.current_job[0].uuid)
169
+ if(last == undefined){
170
+ ns = this.get_idle()
171
+ this.current_job = []
172
+ }else{
173
+ ns = [last]
174
+ if(ns[0].websocket.readyState != 1){
175
+ ns = this.get_idle()
176
+ this.current_job = []
177
+ }
178
+ }
179
+ }else{
180
+ // First time
181
+ this.sync_local_para(this.localPara!)
182
+ ns = this.get_idle()
183
+ if(ns.length > 0) {
184
+ this.proxy?.executeTaskStart([task, taskCount ])
185
+ this.proxy?.executeSubtaskStart([task, 0, ns[0].uuid])
186
+ }
187
+ }
188
+
189
+ if (ns.length > 0 && ns[0].websocket.readyState == 1 && this.check_socket_state(ns[0]) != ExecuteState.RUNNING)
190
+ {
191
+ if(this.check_single_end()){
192
+ allJobFinish = true
193
+ }else{
194
+ if(this.current_job.length != task.jobs.length){
195
+ const job:Job = JSON.parse(JSON.stringify(task.jobs[this.current_job.length]))
196
+ const runtime = uuidv6()
197
+ this.current_job.push({
198
+ uuid: ns[0].uuid,
199
+ runtime: runtime,
200
+ state: ExecuteState.RUNNING,
201
+ job: job
202
+ })
203
+ job.index = 0
204
+ job.runtime_uuid = runtime
205
+ this.ExecuteJob(project, task, job, ns[0], false)
206
+ }
207
+ }
208
+ }
209
+ return allJobFinish
210
+ }
211
+
212
+ private ExecuteTask_Setup(project:Project, task:Task, taskCount:number):boolean {
213
+ let ns:Array<WebsocketPack> = this.get_idle_open()
214
+ let allJobFinish = false
215
+
216
+ /**
217
+ * if current_cron length is zero\
218
+ * this means the init process has not been run yet
219
+ */
220
+ if(this.current_cron.length == 0){
221
+ // First time
222
+ this.Init_CronContainer(task, taskCount)
223
+ this.messager_log(`[Execute] TaskCount: ${taskCount}`)
224
+ for(let i = 0; i < this.current_cron.length; i++){
225
+ this.current_cron[i].uuid = this.current_nodes[i].uuid
226
+ }
227
+ }
228
+
229
+ if(this.check_all_cron_end()){
230
+ allJobFinish = true
231
+ }else{
232
+ const single = this.current_cron.filter(x => x.uuid != '')
233
+ // Execute
234
+ for(var cronwork of single){
235
+ const index = this.current_nodes.findIndex(x => x.uuid == cronwork.uuid)
236
+ if(index != -1){
237
+ this.ExecuteCronTask(project, task, cronwork, this.current_nodes[index])
238
+ }
239
+ }
240
+ }
241
+ return allJobFinish
242
+ }
243
+
244
+ private ExecuteTask_AllFinish(project:Project, task:Task){
245
+ this.proxy?.executeTaskFinish(task)
246
+ this.messager_log(`[Execute] Task Finish ${task.uuid}`)
247
+ const index = project.task.findIndex(x => x.uuid == task.uuid)
248
+ if(index == project.task.length - 1){
249
+ // Finish
250
+ this.current_t = undefined
251
+ this.t_state = ExecuteState.FINISH
252
+ }else{
253
+ // Next
254
+ this.current_t = project.task[index + 1]
255
+ this.t_state = ExecuteState.NONE
256
+ }
257
+ this.current_job = []
258
+ this.current_cron = []
259
+ }
260
+
261
+ private ExecuteCronTask = (project:Project, task:Task, work:CronJobState, ns:WebsocketPack) => {
262
+ if(ns.current_job.length < this.current_multithread){
263
+ const rindex = work.work.findIndex(x => x.state == ExecuteState.RUNNING)
264
+ if(rindex != -1) return
265
+ const index = work.work.findIndex(x => x.state == ExecuteState.NONE)
266
+ if(index == 0) this.proxy?.executeSubtaskStart([task, work.id, ns.uuid ])
267
+ if(index == -1) return
268
+ work.work[index].state = ExecuteState.RUNNING
269
+ try {
270
+ const job:Job = JSON.parse(JSON.stringify(task.jobs[index]))
271
+ job.index = work.id
272
+ job.runtime_uuid = work.work[index].runtime
273
+ this.ExecuteJob(project, task, job, ns, true)
274
+ }catch(err){
275
+ this.messager_log(`[ExecuteCronTask Error] UUID: ${task.uuid}, Job count: ${task.jobs.length}, index: ${index}`)
276
+ }
277
+ }
278
+ }
279
+
280
+ private ExecuteJob = (project:Project, task:Task, job:Job, wss:WebsocketPack, iscron:boolean) => {
281
+ const n:number = job.index!
282
+ this.messager_log(`[Execute] Job Start ${n} ${job.uuid} ${wss.uuid}`)
283
+ this.proxy?.executeJobStart([ job, n, wss.uuid ])
284
+
285
+ ExecuteManager_Runner.string_args_transform(task, job, this.messager_log, this.localPara!, n)
286
+ const h:Header = {
287
+ name: 'execute_job',
288
+ channel: this.uuid,
289
+ data: job
290
+ }
291
+ wss.current_job.push(job.runtime_uuid!)
292
+ const stringdata = JSON.stringify(h)
293
+ wss.websocket.send(stringdata)
294
+ this.jobstack = this.jobstack + 1
295
+ }
296
+
297
+ /**
298
+ * Boradcasting all the parameter and library to all the websocket nodes
299
+ * @param p Target project
300
+ */
301
+ SyncParameter = (p:Project) => {
302
+ // Get the clone para from it
303
+ this.localPara = JSON.parse(JSON.stringify(p.parameter))
304
+ this.messager_log("[Execute] Sync Parameter !")
305
+ this.messager_log("[Execute] Generate local parameter object")
306
+ // Then phrase the expression to value
307
+ for(let i = 0; i < this.localPara!.containers.length; i++){
308
+ if(this.localPara!.containers[i].type == DataType.Expression && this.localPara!.containers[i].meta != undefined){
309
+ const text = `%{${this.localPara!.containers[i].meta}}%`
310
+ const e = new Util_Parser([...Util_Parser.to_keyvalue(this.localPara!)])
311
+ this.localPara!.containers[i].value = e.replacePara(text)
312
+ }
313
+ }
314
+ // Boradcasting
315
+ this.sync_local_para(this.localPara!)
316
+ }
317
+
318
+
319
+ protected Init_CronContainer = (task:Task, taskCount:number) => {
320
+ this.sync_local_para(this.localPara!)
321
+ this.current_cron = []
322
+ // Create the cronjob instance here
323
+ for(let i = 0; i < taskCount; i++){
324
+ const d:CronJobState = {
325
+ id: i,
326
+ uuid: "",
327
+ work: task.jobs.map(x => ({
328
+ uuid: x.uuid,
329
+ runtime: '',
330
+ state: ExecuteState.NONE,
331
+ job: x
332
+ }))
333
+ }
334
+ d.work.forEach((x, j) => x.runtime = uuidv6({}, undefined, i * taskCount + j))
335
+ this.current_cron.push(d)
336
+ }
337
+ this.proxy?.executeTaskStart([task, taskCount ])
338
+ }
339
+ }
@@ -0,0 +1,175 @@
1
+ // ========================
2
+ //
3
+ // Share Codebase
4
+ //
5
+ // ========================
6
+ import { formula, init } from "expressionparser";
7
+ import { DataType, ENV_CHARACTER, IGNORE_CHARACTER, KeyValue, Parameter, ParameterContainer } from "../../interface";
8
+
9
+ /**
10
+ * The worker which helps parsing parameter variables into argument\
11
+ * Including expression executing
12
+ */
13
+ export class Util_Parser {
14
+
15
+ paras:Array<KeyValue> = []
16
+ public get count() : number {
17
+ return this.paras.length
18
+ }
19
+
20
+ constructor(_paras:Array<KeyValue>){
21
+ this.paras = _paras
22
+ }
23
+
24
+ clone = () => {
25
+ const b:Array<KeyValue> = JSON.parse(JSON.stringify(this.paras))
26
+ return new Util_Parser(b)
27
+ }
28
+
29
+ /**
30
+ * Turn parameter into a list of keyvalue structure\
31
+ * Exclude the expression datatype
32
+ * @param p Target parameter instance
33
+ * @returns The list of keyvalue
34
+ */
35
+ static to_keyvalue = (p:Parameter):Array<KeyValue> => {
36
+ return [
37
+ ...this._to_keyvalue(p.containers)
38
+ ]
39
+ }
40
+
41
+ /**
42
+ * Input a object data, and deep search all of subobject\
43
+ * Phrasing it into keyvalue data
44
+ * @param obj Object
45
+ * @returns Array of keyvalue data
46
+ */
47
+ private static getDeepKeys = (obj:any, name?:string):Array<[string, any]> => {
48
+ let keys:Array<[string, any]> = []
49
+ for(var key in obj) {
50
+ keys.push([name ? name + "." + key : key, obj[key]]);
51
+ if(typeof obj[key] === "object") {
52
+ if(Array.isArray(obj[key])) {
53
+ if(typeof obj[key]['length'] === 'number'){
54
+ keys.push([name ? name + "." + key + ".length" : key + ".length", obj[key]['length']]);
55
+ }
56
+ }
57
+ var subkeys = this.getDeepKeys(obj[key]);
58
+ keys = keys.concat(subkeys.map(function(subkey) {
59
+ return [name ? name + "." + key + "." + subkey[0] : key + "." + subkey[0], subkey[1]];
60
+ }));
61
+ }
62
+ }
63
+ return keys
64
+ }
65
+
66
+ /**
67
+ * Parameter containers into keyvalue list
68
+ */
69
+ static _to_keyvalue = (p:Array<ParameterContainer>):Array<KeyValue> => {
70
+ const r:Array<KeyValue> = []
71
+ r.push(...p.filter(x => x.type == DataType.Boolean || x.type == DataType.String || x.type == DataType.Textarea || x.type == DataType.Number || x.type == DataType.Expression).map(x => { return { key: x.name, value: x.value.toString() } }))
72
+ const objs = p.filter(x => x.type == DataType.Object)
73
+ const lists = p.filter(x => x.type == DataType.List)
74
+ const selects = p.filter(x => x.type == DataType.Select)
75
+ for(const obj of objs){
76
+ const v = obj.value
77
+ const keys = this.getDeepKeys(v, obj.name)
78
+ r.push(...keys.map(x => { return { key: x[0], value: x[1].toString() } }))
79
+ }
80
+ for(const list of lists){
81
+ const a:Array<any> = list.value
82
+ r.push(...a.map((x, index) => { return { key: list.name + "." + String(index), value: x } }))
83
+ r.push({ key: list.name + ".length", value: a.length })
84
+ }
85
+ for(const select of selects){
86
+ const a:Array<any> = select.meta
87
+ const target = a[select.value]
88
+ r.push({ key: select.name, value: target })
89
+ }
90
+ return r
91
+ }
92
+
93
+ /**
94
+ * Search all the string result and replace to target string\
95
+ * @example
96
+ * replaceAll("ABCBCAB", "AB", "KK") // Result: KKCBCKK
97
+ * @param str string data
98
+ * @param fi feature
99
+ * @param tar replace target
100
+ */
101
+ static replaceAll = (str:string, fi:string, tar:string):string => {
102
+ let p = str
103
+ while(p.includes(fi)) p = p.replace(fi, tar)
104
+ return p
105
+ }
106
+
107
+ /**
108
+ * Replace a string to environment string\
109
+ * * Include Expression calculation
110
+ * * Include Env string, boolean, number replacing
111
+ * @param text Input text
112
+ * @param paras The keyvalue list
113
+ * @returns The result string
114
+ */
115
+ replacePara = (text:string):string => {
116
+ let buffer = ''
117
+ let store = ''
118
+ let state:boolean = false
119
+ let ignore:number = -1
120
+ let useExp = false
121
+ for(const v of text){
122
+ if(v == IGNORE_CHARACTER && ignore == -1) ignore = 0
123
+ else if(ignore == 0) ignore = 1
124
+ else if(ignore == 1) ignore = 2
125
+ else if(ignore == 2) ignore = -1
126
+ if(v == ENV_CHARACTER && ignore == -1){
127
+ state = !state
128
+ if(!state) { // End
129
+ if(useExp){
130
+ buffer += this.parse(store)
131
+ }else{
132
+ buffer += this._replacePara(store)
133
+ }
134
+ store = ""
135
+ useExp = false
136
+ }
137
+ }
138
+ if(v == '{' && state && store.length == 0) useExp = true
139
+ if(state && v != ENV_CHARACTER && (ignore != 0)) store += v
140
+ if(!state && v != ENV_CHARACTER && (ignore != 0)) buffer += (ignore > 0 ? (ENV_CHARACTER + v) : v)
141
+ }
142
+ return buffer
143
+ }
144
+
145
+ /**
146
+ * Expression magic
147
+ * @param str Input string, the expression part of string only, not the entire sentence
148
+ * @param paras Keyvalue list
149
+ * @returns Result calculation
150
+ */
151
+ parse = (str:string):string => {
152
+ str = str.substring(1, str.length - 1)
153
+ const parser = init(formula, (term: string) => {
154
+ if(term.includes("_ck_")){
155
+ const index = this.paras.findIndex(x => x.key == "ck")
156
+ if(index != -1) term = Util_Parser.replaceAll(term, "_ck_", this.paras[index].value)
157
+ }
158
+ const index = this.paras.findIndex(x => x.key == term)
159
+ if(index != -1) {
160
+ const n = Number(this.paras[index].value)
161
+ if(Number.isNaN(n)) return this.paras[index].value
162
+ return n
163
+ }
164
+ else return 0
165
+ });
166
+ const r = parser.expressionToValue(str).toString()
167
+ return r
168
+ }
169
+
170
+ private _replacePara = (store:string) => {
171
+ const index = this.paras.findIndex(x => x.key == store)
172
+ if(index == -1) return `%${store}%`
173
+ return this.paras[index].value
174
+ }
175
+ }