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.
- package/LICENSE +21 -0
- package/README.md +2 -0
- package/bun.lock +730 -0
- package/index.ts +1 -0
- package/jest.config.js +8 -0
- package/package.json +42 -0
- package/src/client/analysis.ts +377 -0
- package/src/client/client.ts +230 -0
- package/src/client/cluster.ts +125 -0
- package/src/client/execute.ts +210 -0
- package/src/client/http.ts +45 -0
- package/src/client/javascript.ts +535 -0
- package/src/client/job_execute.ts +216 -0
- package/src/client/job_parameter.ts +41 -0
- package/src/client/os.ts +210 -0
- package/src/client/parameter.ts +58 -0
- package/src/client/resource.ts +121 -0
- package/src/client/shell.ts +147 -0
- package/src/interface/base.ts +82 -0
- package/src/interface/bus.ts +144 -0
- package/src/interface/enum.ts +181 -0
- package/src/interface/execute.ts +47 -0
- package/src/interface/record.ts +131 -0
- package/src/interface/server.ts +91 -0
- package/src/interface/struct.ts +292 -0
- package/src/interface/table.ts +34 -0
- package/src/interface/ui.ts +35 -0
- package/src/interface.ts +50 -0
- package/src/lan/en.json +395 -0
- package/src/lan/zh_TW.json +395 -0
- package/src/plugins/i18n.ts +20 -0
- package/src/script/console_manager.ts +135 -0
- package/src/script/console_server_manager.ts +46 -0
- package/src/script/execute/base.ts +309 -0
- package/src/script/execute/feedback.ts +212 -0
- package/src/script/execute/region_job.ts +14 -0
- package/src/script/execute/region_project.ts +23 -0
- package/src/script/execute/region_subtask.ts +14 -0
- package/src/script/execute/region_task.ts +23 -0
- package/src/script/execute/runner.ts +339 -0
- package/src/script/execute/util_parser.ts +175 -0
- package/src/script/execute_manager.ts +348 -0
- package/src/script/socket_manager.ts +329 -0
- package/src/script/webhook_manager.ts +6 -0
- package/src/script/webhook_server_manager.ts +102 -0
- package/src/util/server/console_handle.ts +248 -0
- package/src/util/server/log_handle.ts +194 -0
- package/test/TEST.ts +63 -0
- package/test/client/execute.test.ts +54 -0
- package/test/client/javascript.test.ts +78 -0
- package/test/client/server.test.ts +26 -0
- package/test/client/task.test.ts +136 -0
- package/test/script/parser.test.ts +110 -0
- package/test/script/socket.test.ts +27 -0
- package/tsconfig.json +15 -0
package/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './src/interface'
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "verteilen-core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"homepage": "https://verteilen.github.io/wiki/",
|
|
6
|
+
"author": "Elly",
|
|
7
|
+
"repository": "https://github.com/Verteilen/Verteilen-Core",
|
|
8
|
+
"description": "The core library help verteilen software to work",
|
|
9
|
+
"main": "main/main.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "jest --coverage"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@types/bcrypt": "^5.0.2",
|
|
15
|
+
"@types/jest": "^30.0.0",
|
|
16
|
+
"@types/pem": "^1.14.4",
|
|
17
|
+
"@types/tcp-port-used": "^1.0.4",
|
|
18
|
+
"@types/ws": "^8.5.14",
|
|
19
|
+
"ts-jest": "^29.3.1",
|
|
20
|
+
"ts-node": "^10.9.2",
|
|
21
|
+
"typescript": "^5.2.2"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@intlify/devtools-types": "^11.1.12",
|
|
25
|
+
"bcrypt": "^5.1.1",
|
|
26
|
+
"browser-or-node": "^3.0.0",
|
|
27
|
+
"byte-size": "^9.0.1",
|
|
28
|
+
"expressionparser": "^1.1.5",
|
|
29
|
+
"jest": "^29.7.0",
|
|
30
|
+
"pem": "^1.14.8",
|
|
31
|
+
"systeminformation": "^5.25.11",
|
|
32
|
+
"tcp-port-used": "^1.0.2",
|
|
33
|
+
"tree-kill": "^1.2.2",
|
|
34
|
+
"tsc": "^2.0.4",
|
|
35
|
+
"uuid": "^11.0.5",
|
|
36
|
+
"vue-i18n": "^11.1.12",
|
|
37
|
+
"ws": "^8.18.0"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/Verteilen/Verteilen-Core/issues"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// ========================
|
|
2
|
+
//
|
|
3
|
+
// Share Codebase
|
|
4
|
+
//
|
|
5
|
+
// ========================
|
|
6
|
+
import { ChildProcess, exec, spawn } from 'child_process';
|
|
7
|
+
import { WebSocket } from 'ws';
|
|
8
|
+
import { DATA_FOLDER, Header, Job, Libraries, Messager, Messager_log, Parameter, Plugin, PluginList, PluginToken, PluginWithToken } from "../interface";
|
|
9
|
+
import { Client } from './client';
|
|
10
|
+
import { ClientExecute } from "./execute";
|
|
11
|
+
import { ClientShell } from './shell';
|
|
12
|
+
import { createWriteStream, existsSync, mkdir, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
import * as os from 'os';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The analysis worker. decode the message received from cluster server
|
|
18
|
+
*/
|
|
19
|
+
export class ClientAnalysis {
|
|
20
|
+
private messager: Messager
|
|
21
|
+
private messager_log: Messager_log
|
|
22
|
+
private client:Client
|
|
23
|
+
private exec:Array<ClientExecute>
|
|
24
|
+
private shell:ClientShell
|
|
25
|
+
private resource_wanter:Array<WebSocket> = []
|
|
26
|
+
private resource_thread:ChildProcess | undefined = undefined
|
|
27
|
+
|
|
28
|
+
private resource_cache:Header | undefined = undefined
|
|
29
|
+
|
|
30
|
+
constructor(_messager:Messager, _messager_log:Messager_log, _client:Client){
|
|
31
|
+
this.client = _client
|
|
32
|
+
this.messager = _messager
|
|
33
|
+
this.messager_log = _messager_log
|
|
34
|
+
this.shell = new ClientShell(_messager, _messager_log, this.client)
|
|
35
|
+
this.exec = []
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Analysis the package
|
|
40
|
+
* @param h Package
|
|
41
|
+
* @param source Websocket instance
|
|
42
|
+
* @return
|
|
43
|
+
* * 0: Successfully execute command
|
|
44
|
+
* * 1: The header is undefined, cannot process
|
|
45
|
+
* * 2: Cannot find the header name match with function typeMap
|
|
46
|
+
*/
|
|
47
|
+
analysis = (h:Header | undefined, source:WebSocket) => {
|
|
48
|
+
const typeMap = {
|
|
49
|
+
'execute_job': this.execute_job,
|
|
50
|
+
'release': this.release,
|
|
51
|
+
'stop_job': this.stop_all,
|
|
52
|
+
'set_parameter': this.set_parameter,
|
|
53
|
+
'set_libs': this.set_libs,
|
|
54
|
+
'shell_folder': this.shell.shell_folder,
|
|
55
|
+
'open_shell': this.shell.open_shell,
|
|
56
|
+
'close_shell': this.shell.close_shell,
|
|
57
|
+
'enter_shell': this.shell.enter_shell,
|
|
58
|
+
'resource_start': this.resource_start,
|
|
59
|
+
'resource_end': this.resource_end,
|
|
60
|
+
'ping': this.pong,
|
|
61
|
+
'plugin_info': this.plugin_info,
|
|
62
|
+
'plugin_download': this.plugin_download,
|
|
63
|
+
'plugin_remove': this.plugin_remove,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (h == undefined){
|
|
67
|
+
this.messager_log('[Client Analysis] Analysis Failed, Value is undefined')
|
|
68
|
+
return 1
|
|
69
|
+
}
|
|
70
|
+
if (h.message != undefined && h.message.length > 0){
|
|
71
|
+
this.messager_log(`[Client Analysis] ${h.message}`)
|
|
72
|
+
}
|
|
73
|
+
if (h.data == undefined) {
|
|
74
|
+
this.messager_log('[Client Analysis] Analysis Warn, Data is undefined')
|
|
75
|
+
h.data = 0
|
|
76
|
+
}
|
|
77
|
+
if(typeMap.hasOwnProperty(h.name)){
|
|
78
|
+
const castingFunc = typeMap[h.name]
|
|
79
|
+
castingFunc(h.data, source, h.channel)
|
|
80
|
+
return 0
|
|
81
|
+
}else{
|
|
82
|
+
this.messager_log(`[Client Analysis] Analysis Failed, Unknowed header, name: ${h.name}, meta: ${h.meta}`)
|
|
83
|
+
return 2
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private execute_job = (job: Job, source: WebSocket, channel:string | undefined) => {
|
|
88
|
+
if(channel == undefined) return
|
|
89
|
+
const target = this.exec_checker(channel)
|
|
90
|
+
target.execute_job(job, source)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private release = (dummy:number, source: WebSocket, channel:string | undefined) => {
|
|
94
|
+
if(channel == undefined) return
|
|
95
|
+
const index = this.exec.findIndex(x => x.uuid == channel)
|
|
96
|
+
if(index == -1) return
|
|
97
|
+
this.exec.splice(index, 1)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private set_parameter = (data:Parameter, source: WebSocket, channel:string | undefined) => {
|
|
101
|
+
if(channel == undefined) return
|
|
102
|
+
const target = this.exec_checker(channel)
|
|
103
|
+
target.set_parameter(data)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private set_libs = (data:Libraries, source: WebSocket, channel:string | undefined) => {
|
|
107
|
+
if(channel == undefined) return
|
|
108
|
+
const target = this.exec_checker(channel)
|
|
109
|
+
target.set_libs(data)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private exec_checker = (uuid:string): ClientExecute => {
|
|
113
|
+
let r:ClientExecute | undefined = undefined
|
|
114
|
+
const index = this.exec.findIndex(x => x.uuid == uuid)
|
|
115
|
+
if(index == -1) {
|
|
116
|
+
r = new ClientExecute(uuid, this.messager, this.messager_log, this.client)
|
|
117
|
+
this.exec.push(r)
|
|
118
|
+
}else{
|
|
119
|
+
r = this.exec[index]
|
|
120
|
+
}
|
|
121
|
+
return r
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Network delay request
|
|
126
|
+
* @param data Dummy value, should always be 0
|
|
127
|
+
* @param source The cluster server websocket instance
|
|
128
|
+
*/
|
|
129
|
+
private pong = (data:number, source: WebSocket) => {
|
|
130
|
+
const h:Header = { name: 'pong', data: data }
|
|
131
|
+
source.send(JSON.stringify(h))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private plugin_info = (data:number, source: WebSocket) => {
|
|
135
|
+
const pat = path.join(os.homedir(), DATA_FOLDER, "plugin.json")
|
|
136
|
+
if(existsSync(pat)){
|
|
137
|
+
const p:PluginList = JSON.parse(readFileSync(pat).toString())
|
|
138
|
+
const h:Header = { name: 'plugin_info_reply', data: p.plugins }
|
|
139
|
+
source.send(JSON.stringify(h))
|
|
140
|
+
}else{
|
|
141
|
+
const p:PluginList = { plugins: [] }
|
|
142
|
+
const h:Header = { name: 'plugin_info_reply', data: p.plugins }
|
|
143
|
+
writeFileSync(pat, JSON.stringify(p))
|
|
144
|
+
source.send(JSON.stringify(h))
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private get_releases = async (repo:string, token:string | undefined) => {
|
|
149
|
+
const qu = await fetch(`https://api.github.com/repos/${repo}/releases`, {
|
|
150
|
+
headers: {
|
|
151
|
+
Authorization: token ? `token ${token}`: '',
|
|
152
|
+
Accept: "application/vnd.github.v3.raw",
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
return qu.text()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private filterout = async (repo:string, token:string | undefined, version:string, filename:string) => {
|
|
159
|
+
const text = await this.get_releases(repo, token)
|
|
160
|
+
const json:Array<any> = JSON.parse(text)
|
|
161
|
+
const v = json.find(x => x.tag_name == version)
|
|
162
|
+
if(!v) return
|
|
163
|
+
const f = v.assets.find(x => x.name == filename)
|
|
164
|
+
if(!f) return
|
|
165
|
+
return f.id
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private write_plugin = (t: string | undefined, plugin:PluginWithToken, source: WebSocket) => {
|
|
169
|
+
const list = this.client.plugins.plugins
|
|
170
|
+
const index = list.findIndex(x => x.name == plugin.name)
|
|
171
|
+
plugin.token = t ? [t] : []
|
|
172
|
+
plugin.progress = 0
|
|
173
|
+
if(index == -1){
|
|
174
|
+
list.push(plugin)
|
|
175
|
+
}else{
|
|
176
|
+
list[index] = plugin
|
|
177
|
+
}
|
|
178
|
+
this.client.savePlugin()
|
|
179
|
+
this.plugin_info(0, source)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private finish_plugin = (plugin:PluginWithToken, source: WebSocket) => {
|
|
183
|
+
const list = this.client.plugins.plugins
|
|
184
|
+
const index = list.findIndex(x => x.name == plugin.name)
|
|
185
|
+
plugin.progress = 1
|
|
186
|
+
if(index == -1){
|
|
187
|
+
list.push(plugin)
|
|
188
|
+
}else{
|
|
189
|
+
list[index] = plugin
|
|
190
|
+
}
|
|
191
|
+
this.client.savePlugin()
|
|
192
|
+
this.plugin_info(0, source)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private plugin_download = async (plugin:PluginWithToken, source: WebSocket) => {
|
|
196
|
+
const target = plugin.contents.find(x => x.arch == process.arch && x.platform == process.platform)
|
|
197
|
+
if(target == undefined){
|
|
198
|
+
this.messager_log(`[Plugin] Cannot find target plugin for ${plugin.name} on ${process.platform} ${process.arch}`)
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
const links = target.url.split('/')
|
|
202
|
+
const filename = links[links.length - 1]
|
|
203
|
+
const version = links[links.length - 2]
|
|
204
|
+
const REPO = `${links[3]}/${links[4]}`
|
|
205
|
+
const dir = path.join(os.homedir(), DATA_FOLDER, "exe")
|
|
206
|
+
if(!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
207
|
+
let req:RequestInit = {}
|
|
208
|
+
const tokens = [undefined, ...plugin.token]
|
|
209
|
+
const fileStream = createWriteStream(path.join(dir, target.filename), { flags: 'a' });
|
|
210
|
+
let pass = false
|
|
211
|
+
for(let t of tokens){
|
|
212
|
+
if(pass) break
|
|
213
|
+
try{
|
|
214
|
+
const id = await this.filterout(REPO, t, version, filename)
|
|
215
|
+
req = {
|
|
216
|
+
method: 'GET',
|
|
217
|
+
credentials: 'include',
|
|
218
|
+
headers: {
|
|
219
|
+
Authorization: t ? `token ${t}` : '',
|
|
220
|
+
Accept: "application/octet-stream"
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const url = `https://api.github.com/repos/${REPO}/releases/assets/${id}`
|
|
224
|
+
fetch(url, req).then(async res => {
|
|
225
|
+
if(!res.ok){
|
|
226
|
+
throw new Error(`Failed to download file: ${res.status} ${res.statusText}`);
|
|
227
|
+
}
|
|
228
|
+
this.write_plugin(t, plugin, source)
|
|
229
|
+
return res.blob()
|
|
230
|
+
}).then(blob => {
|
|
231
|
+
return blob.stream().getReader().read()
|
|
232
|
+
})
|
|
233
|
+
.then(reader => {
|
|
234
|
+
if(!reader.done){
|
|
235
|
+
fileStream.write(Buffer.from(reader.value))
|
|
236
|
+
}
|
|
237
|
+
}).finally(() => {
|
|
238
|
+
this.messager_log(`[Plugin] Downloaded ${plugin.name} successfully`)
|
|
239
|
+
fileStream.end();
|
|
240
|
+
if(process.platform == 'linux'){
|
|
241
|
+
exec(`chmod +x ${path.join(dir, target.filename)}`, (err) => {
|
|
242
|
+
this.messager_log(`[Plugin] Permission failed ${err?.message}`)
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
this.finish_plugin(plugin, source)
|
|
246
|
+
pass = true
|
|
247
|
+
})
|
|
248
|
+
}
|
|
249
|
+
catch(err:any){
|
|
250
|
+
this.messager_log(`[Plugin] Download failed for ${plugin.name}: ${err.message}`)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private plugin_remove = (plugin:Plugin, source: WebSocket) => {
|
|
256
|
+
this.client.plugins.plugins = this.client.plugins.plugins.filter(x => x.name != plugin.name)
|
|
257
|
+
this.client.savePlugin()
|
|
258
|
+
const dir = path.join(os.homedir(), DATA_FOLDER, "exe")
|
|
259
|
+
if(!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
260
|
+
plugin.contents.forEach(x => {
|
|
261
|
+
if(existsSync(path.join(dir, x.filename))){
|
|
262
|
+
rmSync(path.join(dir, x.filename))
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
this.plugin_info(0, source)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private resource_start = (data:number, source: WebSocket) => {
|
|
269
|
+
this.resource_wanter.push(source)
|
|
270
|
+
this.messager_log(`Register resource_wanter!, count: ${this.resource_wanter.length}`)
|
|
271
|
+
if(this.resource_cache != undefined) source.send(JSON.stringify(this.resource_cache))
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private resource_end = (data:number, source: WebSocket) => {
|
|
275
|
+
const index = this.resource_wanter.findIndex(x => x ==source)
|
|
276
|
+
if(index != -1) {
|
|
277
|
+
this.resource_wanter.splice(index, 1)
|
|
278
|
+
this.messager_log(`UnRegister resource_wanter!, count: ${this.resource_wanter.length}`)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
update = (client:Client) => {
|
|
283
|
+
this.resource_require()
|
|
284
|
+
if(this.resource_cache != undefined){
|
|
285
|
+
this.resource_wanter.forEach(x => x.send(JSON.stringify(this.resource_cache)))
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
disconnect = (source: WebSocket) => {
|
|
290
|
+
this.shell.disconnect(source)
|
|
291
|
+
this.exec.forEach(x => x.stop_job())
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
stop_all = () => {
|
|
295
|
+
this.exec.forEach(x => x.stop_job())
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
destroy = () => {
|
|
299
|
+
if(this.resource_thread != undefined) this.resource_thread.kill()
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private resource_require = () => {
|
|
303
|
+
if(this.resource_thread != undefined) return
|
|
304
|
+
const shouldRun = this.resource_thread == undefined && (this.resource_cache == undefined || this.resource_wanter.length > 0)
|
|
305
|
+
if(!shouldRun) return
|
|
306
|
+
this.resource_thread = spawn(Client.workerPath(), [],
|
|
307
|
+
{
|
|
308
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
309
|
+
shell: true,
|
|
310
|
+
windowsHide: true,
|
|
311
|
+
env: {
|
|
312
|
+
...process.env,
|
|
313
|
+
type: "RESOURCE",
|
|
314
|
+
cache: this.resource_cache == undefined ? undefined : JSON.stringify(this.resource_cache.data)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
)
|
|
318
|
+
let k = ""
|
|
319
|
+
|
|
320
|
+
const workerFeedbackExec = (str:string) => {
|
|
321
|
+
try{
|
|
322
|
+
const msg:Header = JSON.parse(str)
|
|
323
|
+
if(msg.name == 'messager'){
|
|
324
|
+
this.messager(msg.data, "RESOURCE")
|
|
325
|
+
}
|
|
326
|
+
else if(msg.name == 'messager_log'){
|
|
327
|
+
this.messager_log(msg.data, "RESOURCE")
|
|
328
|
+
}
|
|
329
|
+
else if(msg.name == 'resource'){
|
|
330
|
+
const h:Header = {
|
|
331
|
+
name: 'system_info',
|
|
332
|
+
data: msg.data
|
|
333
|
+
}
|
|
334
|
+
this.resource_cache = h
|
|
335
|
+
this.resource_wanter.forEach(x => x.send(JSON.stringify(h)))
|
|
336
|
+
}
|
|
337
|
+
else if(msg.name == 'error'){
|
|
338
|
+
if(msg.data instanceof String) this.messager_log(msg.data.toString(), "RESOURCE")
|
|
339
|
+
else this.messager_log(JSON.stringify(msg.data), "RESOURCE")
|
|
340
|
+
}
|
|
341
|
+
}catch(err:any){
|
|
342
|
+
console.log("str: " + str)
|
|
343
|
+
console.log(err.name + "\n" + err.message)
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const workerFeedback = (str:string) => {
|
|
347
|
+
for(let i = 0; i < str.length; i++){
|
|
348
|
+
if(str[i] != '\n') k += str[i]
|
|
349
|
+
else {
|
|
350
|
+
workerFeedbackExec(k)
|
|
351
|
+
k = ''
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
this.resource_thread.on('error', (err) => {
|
|
357
|
+
this.messager_log(`[Worker Error] ${err}`)
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
this.resource_thread.on('exit', (code, signal) => {
|
|
361
|
+
this.resource_thread = undefined
|
|
362
|
+
})
|
|
363
|
+
this.resource_thread.on('message', (message, sendHandle) => {
|
|
364
|
+
workerFeedback(message.toString())
|
|
365
|
+
})
|
|
366
|
+
this.resource_thread.stdout?.setEncoding('utf8');
|
|
367
|
+
this.resource_thread.stdout?.on('data', (chunk) => {
|
|
368
|
+
workerFeedback(chunk.toString())
|
|
369
|
+
})
|
|
370
|
+
this.resource_thread.stderr?.setEncoding('utf8');
|
|
371
|
+
this.resource_thread.stderr?.on('data', (chunk) => {
|
|
372
|
+
workerFeedback(chunk.toString())
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// ========================
|
|
2
|
+
//
|
|
3
|
+
// Share Codebase
|
|
4
|
+
//
|
|
5
|
+
// ========================
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { check } from 'tcp-port-used';
|
|
8
|
+
import { WebSocket } from 'ws';
|
|
9
|
+
import * as ws from 'ws';
|
|
10
|
+
import { CLIENT_UPDATETICK, DATA_FOLDER, Header, Messager, Messager_log, Plugin, PluginList, PORT } from '../interface';
|
|
11
|
+
import { ClientAnalysis } from './analysis';
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
13
|
+
import * as os from 'os'
|
|
14
|
+
import * as pem from 'pem'
|
|
15
|
+
import * as https from 'https'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The calculation node worker
|
|
19
|
+
*/
|
|
20
|
+
export class Client {
|
|
21
|
+
plugins: PluginList = { plugins: [] }
|
|
22
|
+
|
|
23
|
+
private httpss:https.Server<any> | undefined = undefined
|
|
24
|
+
private client:ws.Server | undefined = undefined
|
|
25
|
+
private sources:Array<WebSocket> = []
|
|
26
|
+
private messager:Messager
|
|
27
|
+
private messager_log:Messager_log
|
|
28
|
+
private analysis:Array<ClientAnalysis>
|
|
29
|
+
private updatehandle
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get connected client count
|
|
33
|
+
*/
|
|
34
|
+
public get count() : number {
|
|
35
|
+
return this.sources.length
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Get connected client list instance
|
|
39
|
+
*/
|
|
40
|
+
public get clients() : Array<WebSocket> {
|
|
41
|
+
return this.sources
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
constructor(_messager:Messager, _messager_log:Messager_log){
|
|
45
|
+
this.messager = _messager
|
|
46
|
+
this.messager_log = _messager_log
|
|
47
|
+
this.analysis = []
|
|
48
|
+
this.updatehandle = setInterval(this.update, CLIENT_UPDATETICK);
|
|
49
|
+
this.loadPlugins()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
Dispose (){
|
|
53
|
+
clearInterval(this.updatehandle)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Start a websocket server, and waiting for cluster server to connect
|
|
58
|
+
*/
|
|
59
|
+
Init = async () => {
|
|
60
|
+
let port_result = PORT
|
|
61
|
+
let canbeuse = false
|
|
62
|
+
|
|
63
|
+
while(!canbeuse){
|
|
64
|
+
await check(port_result).then(x => {
|
|
65
|
+
canbeuse = !x
|
|
66
|
+
}).catch(err => {
|
|
67
|
+
canbeuse = true
|
|
68
|
+
})
|
|
69
|
+
if(!canbeuse) port_result += 1
|
|
70
|
+
}
|
|
71
|
+
const pems = await this.get_pem()
|
|
72
|
+
this.httpss = https.createServer({ key: pems[0], cert: pems[1], minVersion: 'TLSv1.2', maxVersion: 'TLSv1.3' }, (req, res) => {
|
|
73
|
+
res.writeHead(200)
|
|
74
|
+
res.end('HTTPS server is running');
|
|
75
|
+
})
|
|
76
|
+
this.httpss.addListener('upgrade', (req, res, head) => console.log('UPGRADE:', req.url))
|
|
77
|
+
this.client = new ws.Server({server: this.httpss})
|
|
78
|
+
this.client.on('listening', () => {
|
|
79
|
+
this.messager_log('[Server] Listen PORT: ' + port_result.toString())
|
|
80
|
+
})
|
|
81
|
+
this.client.on('error', (err) => {
|
|
82
|
+
this.messager_log(`[Server] Error ${err.name}\n\t${err.message}\n\t${err.stack}`)
|
|
83
|
+
})
|
|
84
|
+
this.client.on('close', () => {
|
|
85
|
+
this.messager_log('[Server] Close !')
|
|
86
|
+
this.Release()
|
|
87
|
+
})
|
|
88
|
+
this.client.on('connection', (ws, request) => {
|
|
89
|
+
const a = new ClientAnalysis(this.messager, this.messager_log, this)
|
|
90
|
+
this.analysis.push(a)
|
|
91
|
+
this.sources.push(ws)
|
|
92
|
+
this.messager_log(`[Server] New Connection detected, ${ws.url}`)
|
|
93
|
+
ws.on('close', (code, reason) => {
|
|
94
|
+
const index = this.sources.findIndex(x => x == ws)
|
|
95
|
+
if(index != -1) this.sources.splice(index, 1)
|
|
96
|
+
this.messager_log(`[Source] Close ${code} ${reason}`)
|
|
97
|
+
a.disconnect(ws)
|
|
98
|
+
})
|
|
99
|
+
ws.on('error', (err) => {
|
|
100
|
+
this.messager_log(`[Source] Error ${err.name}\n\t${err.message}\n\t${err.stack}`)
|
|
101
|
+
})
|
|
102
|
+
ws.on('open', () => {
|
|
103
|
+
this.messager_log(`[Source] New source is connected, URL: ${ws?.url}`)
|
|
104
|
+
})
|
|
105
|
+
ws.on('message', (data, isBinery) => {
|
|
106
|
+
const h:Header | undefined = JSON.parse(data.toString());
|
|
107
|
+
a.analysis(h, ws);
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
this.httpss.listen(port_result, () => {
|
|
111
|
+
this.messager_log('[Server] Select Port: ' + port_result.toString())
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
Destroy = () => {
|
|
116
|
+
if(this.client == undefined) return
|
|
117
|
+
this.client.close((err) => {
|
|
118
|
+
this.messager_log(`[Client] Close error ${err}`)
|
|
119
|
+
})
|
|
120
|
+
this.Release()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
Release = () => {
|
|
124
|
+
this.analysis.forEach(x => x.stop_all())
|
|
125
|
+
this.analysis.forEach(x => x.destroy())
|
|
126
|
+
this.analysis = []
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
savePlugin = () => {
|
|
130
|
+
const f = path.join(os.homedir(), DATA_FOLDER)
|
|
131
|
+
const pluginPath = path.join(f, 'plugin.json')
|
|
132
|
+
if(!existsSync(f)) mkdirSync(f, { recursive: true })
|
|
133
|
+
writeFileSync(pluginPath, JSON.stringify(this.plugins, null, 4))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The node update function, It will do things below
|
|
138
|
+
* * Send system info to cluster server
|
|
139
|
+
*/
|
|
140
|
+
private update = () => {
|
|
141
|
+
this.analysis.forEach(x => x.update(this))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private loadPlugins = () => {
|
|
145
|
+
const f = path.join(os.homedir(), DATA_FOLDER)
|
|
146
|
+
const pluginPath = path.join(f, 'plugin.json')
|
|
147
|
+
if(!existsSync(f)) mkdirSync(f, { recursive: true })
|
|
148
|
+
if(!existsSync(pluginPath)){
|
|
149
|
+
writeFileSync(pluginPath, JSON.stringify(this.plugins, null, 4))
|
|
150
|
+
}else{
|
|
151
|
+
this.plugins = JSON.parse(readFileSync(pluginPath).toString())
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private get_pem = ():Promise<[string, string]> => {
|
|
156
|
+
return new Promise<[string, string]>((resolve) => {
|
|
157
|
+
const pemFolder = path.join(os.homedir(), DATA_FOLDER, 'pem')
|
|
158
|
+
if(!existsSync(pemFolder)) mkdirSync(pemFolder)
|
|
159
|
+
const clientKey = path.join(pemFolder, "client_clientkey.pem")
|
|
160
|
+
const certificate = path.join(pemFolder, "client_certificate.pem")
|
|
161
|
+
if(!existsSync(clientKey) || !existsSync(certificate)){
|
|
162
|
+
pem.createCertificate({selfSigned: true}, (err, keys) => {
|
|
163
|
+
writeFileSync(clientKey, keys.clientKey, { encoding: 'utf8' })
|
|
164
|
+
writeFileSync(certificate, keys.certificate, { encoding: 'utf8' })
|
|
165
|
+
resolve([keys.clientKey, keys.certificate])
|
|
166
|
+
})
|
|
167
|
+
}else{
|
|
168
|
+
resolve([readFileSync(clientKey, 'utf8').toString(), readFileSync(certificate, 'utf8').toString()])
|
|
169
|
+
}
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
public static workerPath = (filename:string = "worker", extension:string = ".exe") => {
|
|
174
|
+
// @ts-ignore
|
|
175
|
+
const isExe = process.pkg?.entrypoint != undefined
|
|
176
|
+
const exe = process.platform == 'win32' ? filename + extension : filename
|
|
177
|
+
let workerExe = ""
|
|
178
|
+
let p = 0
|
|
179
|
+
if(isExe && path.basename(process.execPath) == (process.platform ? "app.exe" : 'app')) { // Node build
|
|
180
|
+
workerExe = path.join(process.execPath, "..", "bin", exe)
|
|
181
|
+
p = 1
|
|
182
|
+
}
|
|
183
|
+
else if(
|
|
184
|
+
(process.mainModule && process.mainModule.filename.indexOf('app.asar') !== -1) ||
|
|
185
|
+
process.argv.filter(a => a.indexOf('app.asar') !== -1).length > 0
|
|
186
|
+
) { // Electron package
|
|
187
|
+
workerExe = path.join("bin", exe)
|
|
188
|
+
p = 2
|
|
189
|
+
}
|
|
190
|
+
else if (process.env.NODE_ENV === 'development'){
|
|
191
|
+
workerExe = path.join(process.cwd(), "bin", exe)
|
|
192
|
+
p = 3
|
|
193
|
+
}
|
|
194
|
+
else{ // Node un-build
|
|
195
|
+
workerExe = Client.isTypescript() ? path.join(__dirname, "bin", exe) : path.join(__dirname, "..", "bin", exe)
|
|
196
|
+
p = 4
|
|
197
|
+
}
|
|
198
|
+
return workerExe
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
static isTypescript = ():boolean => {
|
|
202
|
+
// if this file is typescript, we are running typescript :D
|
|
203
|
+
// this is the best check, but fails when actionhero is compiled to js though...
|
|
204
|
+
const extension = path.extname(__filename);
|
|
205
|
+
if (extension === ".ts") {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// are we running via a ts-node/ts-node-dev shim?
|
|
210
|
+
const lastArg = process.execArgv[process.execArgv.length - 1];
|
|
211
|
+
if (lastArg && path.parse(lastArg).name.indexOf("ts-node") > 0) {
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
/**
|
|
217
|
+
* Are we running in typescript at the moment?
|
|
218
|
+
* see https://github.com/TypeStrong/ts-node/pull/858 for more details
|
|
219
|
+
*/
|
|
220
|
+
return process[Symbol.for("ts-node.register.instance")] ||
|
|
221
|
+
(process.env.NODE_ENV === "test" &&
|
|
222
|
+
process.env.ACTIONHERO_TEST_FILE_EXTENSION !== "js")
|
|
223
|
+
? true
|
|
224
|
+
: false;
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.error(error);
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|