jsir 2.3.2 → 2.3.4
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/cmd/oaa.js +47 -37
- package/deps/room.js +339 -0
- package/deps/server.js +110 -0
- package/deps/setting.js +6 -3
- package/deps/util.js +147 -26
- package/package.json +1 -1
package/cmd/oaa.js
CHANGED
|
@@ -11,7 +11,7 @@ const {
|
|
|
11
11
|
createConsole, setTips, delTips,
|
|
12
12
|
getEditor, errorStr, getConfigDir,
|
|
13
13
|
getFullPath, parseUniqueName, toUniqueName, isJsirFileName, toJsirFileName,
|
|
14
|
-
getAlias, wrapperJsirText, eia
|
|
14
|
+
getAlias, wrapperJsirText, eia, getKeyTips, getValTips, getRoomsDir
|
|
15
15
|
} = $lib;
|
|
16
16
|
const _args = process.argv.slice(2).map(trim);
|
|
17
17
|
const evalCode = require('../deps/evalCode')
|
|
@@ -28,6 +28,7 @@ const _libDataDir = getLibDataDir()
|
|
|
28
28
|
const _fileWatcherMap = {}
|
|
29
29
|
const _types = setting.fileType
|
|
30
30
|
const console = createConsole();
|
|
31
|
+
const room = require('../deps/room');
|
|
31
32
|
|
|
32
33
|
let lastFilterArg = '';
|
|
33
34
|
let _cmdMapFile = setting.name + 'CmdMap.json'
|
|
@@ -345,28 +346,7 @@ function initRl(callback, promptStr, hidden) {
|
|
|
345
346
|
}
|
|
346
347
|
|
|
347
348
|
function getTipStr(showKey = false) {
|
|
348
|
-
let
|
|
349
|
-
for (let key of Object.keys(setting.tips)) {
|
|
350
|
-
let val = setting.tips[key].map(i => {
|
|
351
|
-
let item = trim(i)
|
|
352
|
-
if (item.indexOf(',') !== -1) {
|
|
353
|
-
item = `[${item}]`;
|
|
354
|
-
}
|
|
355
|
-
return item;
|
|
356
|
-
}).join("|");
|
|
357
|
-
key = trim(key)
|
|
358
|
-
|
|
359
|
-
let item
|
|
360
|
-
if (!showKey && vl(val)) {
|
|
361
|
-
item = val;
|
|
362
|
-
} else if (vl(key)) {
|
|
363
|
-
item = key
|
|
364
|
-
}
|
|
365
|
-
if (item) {
|
|
366
|
-
items.push(item)
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
let tips = items.join(',')
|
|
349
|
+
let tips = showKey ? getKeyTips().join(","):getValTips().join(",")
|
|
370
350
|
if (!showKey && !_isTipsDoneShowKey && tips.length > 64) {
|
|
371
351
|
return '...'
|
|
372
352
|
}
|
|
@@ -380,6 +360,10 @@ function closeRl() {
|
|
|
380
360
|
}
|
|
381
361
|
|
|
382
362
|
function _nextLine(callback, promptStr, hidden, resolve, end, isText) {
|
|
363
|
+
if (!setting.enableNextLine) {
|
|
364
|
+
console.$log(warnStr("[warn]"), "NextLine Disabled");
|
|
365
|
+
return
|
|
366
|
+
}
|
|
383
367
|
end = trim(end)
|
|
384
368
|
if (!_haveWrapperInput) {
|
|
385
369
|
return
|
|
@@ -1345,6 +1329,7 @@ const keywordDef = {
|
|
|
1345
1329
|
quit: {
|
|
1346
1330
|
comment: 'Exit',
|
|
1347
1331
|
exeFn: (args) => {
|
|
1332
|
+
room.offRoom();
|
|
1348
1333
|
delTips();
|
|
1349
1334
|
console.log(infoStr("Bye!"));
|
|
1350
1335
|
_noAppendNextLine = true;
|
|
@@ -1417,6 +1402,44 @@ const keywordDef = {
|
|
|
1417
1402
|
}
|
|
1418
1403
|
},
|
|
1419
1404
|
short: 'D'
|
|
1405
|
+
},
|
|
1406
|
+
room: {
|
|
1407
|
+
comment: 'manage room',
|
|
1408
|
+
exeFn: async (args) => {
|
|
1409
|
+
let results = [];
|
|
1410
|
+
await room.syncSetting();
|
|
1411
|
+
for (let i = 0; i < setting.rooms.length; i++) {
|
|
1412
|
+
let room = setting.rooms[i]
|
|
1413
|
+
results.push({
|
|
1414
|
+
mid: i,
|
|
1415
|
+
name: (room.local ? "*":" ") + room.name,
|
|
1416
|
+
node: room.selfNode,
|
|
1417
|
+
active: room.active,
|
|
1418
|
+
})
|
|
1419
|
+
for (let key of Object.keys(room.jsirs)) {
|
|
1420
|
+
let jsir = room.jsirs[key]
|
|
1421
|
+
results.push({
|
|
1422
|
+
mid: i,
|
|
1423
|
+
name: (room.local ? "*":"") + room.name,
|
|
1424
|
+
node: room.selfNode,
|
|
1425
|
+
active: room.active,
|
|
1426
|
+
pid: (process.pid === jsir.pid ? "*":" ") + jsir.pid,
|
|
1427
|
+
space: jsir.space,
|
|
1428
|
+
running: jsir.active,
|
|
1429
|
+
port: jsir.port,
|
|
1430
|
+
back: jsir.back,
|
|
1431
|
+
busy: jsir.busy,
|
|
1432
|
+
tips: Object.keys(jsir.tips).map(i => i + ': ' + jsir.tips[i]).join('\n')
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
if (results.length > 0) {
|
|
1437
|
+
console.nable(results)
|
|
1438
|
+
} else {
|
|
1439
|
+
console.warn("no items")
|
|
1440
|
+
}
|
|
1441
|
+
},
|
|
1442
|
+
short: 'm'
|
|
1420
1443
|
}
|
|
1421
1444
|
}
|
|
1422
1445
|
|
|
@@ -2085,20 +2108,9 @@ async function evalText($text = '', $cmdName = '', $args = []) {
|
|
|
2085
2108
|
$homeDir, $lib, _cmdMap);
|
|
2086
2109
|
}
|
|
2087
2110
|
|
|
2088
|
-
function clearFileLock() {
|
|
2089
|
-
for (let file of Object.keys(fileLockMap)) {
|
|
2090
|
-
fp.rmdir(file)
|
|
2091
|
-
delete setting.fileLock[file]
|
|
2092
|
-
}
|
|
2093
|
-
}
|
|
2094
|
-
|
|
2095
2111
|
function sigExit() {
|
|
2096
|
-
if (!setting.sigExit) {
|
|
2097
|
-
return;
|
|
2098
|
-
}
|
|
2099
2112
|
if (_noAppendNextLine) {
|
|
2100
2113
|
delTips();
|
|
2101
|
-
clearFileLock();
|
|
2102
2114
|
process.exit(0);
|
|
2103
2115
|
} else {
|
|
2104
2116
|
nextLine();
|
|
@@ -2107,15 +2119,12 @@ function sigExit() {
|
|
|
2107
2119
|
|
|
2108
2120
|
process.on('uncaughtException',function(err){
|
|
2109
2121
|
console.$error('uncaughtException', err)
|
|
2110
|
-
_noAppendNextLine || nextLine()
|
|
2111
2122
|
})
|
|
2112
2123
|
process.on('unhandledRejection',function(err){
|
|
2113
2124
|
console.$error('unhandledRejection', err)
|
|
2114
|
-
_noAppendNextLine || nextLine()
|
|
2115
2125
|
})
|
|
2116
2126
|
process.on('rejectionHandled',function(err){
|
|
2117
2127
|
console.$error('rejectionHandled', err)
|
|
2118
|
-
_noAppendNextLine || nextLine()
|
|
2119
2128
|
})
|
|
2120
2129
|
process.on('SIGINT', sigExit);
|
|
2121
2130
|
process.on('SIGTERM', sigExit);
|
|
@@ -2124,6 +2133,7 @@ process.on('beforeExit', function () {
|
|
|
2124
2133
|
delTips();
|
|
2125
2134
|
} else {
|
|
2126
2135
|
nextLine();
|
|
2136
|
+
room.onRoom()
|
|
2127
2137
|
}
|
|
2128
2138
|
});
|
|
2129
2139
|
|
package/deps/room.js
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const {fileJson, fileLock, vl, createConsole, getKeyTips, getValTips,
|
|
3
|
+
getRoomsDir, e, regEach} = require('./util');
|
|
4
|
+
const server = require('../deps/server')
|
|
5
|
+
const roomDataFile = "jsirRoom.json"
|
|
6
|
+
const roomsDirLockKey = "RW_" + getRoomsDir();
|
|
7
|
+
const syncRoomsLockKey = "SyncRooms_" + getRoomsDir();
|
|
8
|
+
const enrichRoomInfoLockKey = "ENRICH_" + roomDataFile
|
|
9
|
+
const console = createConsole();
|
|
10
|
+
const net = require("net");
|
|
11
|
+
const setting = require('../deps/setting')
|
|
12
|
+
const fp = require('fs').promises
|
|
13
|
+
const http = require('http');
|
|
14
|
+
let tailscalePath = os.platform() === 'darwin' ?
|
|
15
|
+
'/Applications/Tailscale.app/Contents/MacOS/Tailscale':'tailscale';
|
|
16
|
+
|
|
17
|
+
function isPidAlive(pid) {
|
|
18
|
+
try {
|
|
19
|
+
process.kill(Number(pid), 0); // 信号 0 不会实际发送,但会检查进程是否存在
|
|
20
|
+
return true; // PID 存在
|
|
21
|
+
} catch (err) {
|
|
22
|
+
if (err.code === 'ESRCH') {
|
|
23
|
+
return false; // 进程不存在
|
|
24
|
+
}
|
|
25
|
+
console.$error(`check isPidAlive ${pid} failed`, err)
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function onRoom() {
|
|
31
|
+
if (!setting.roomTid[0]) {
|
|
32
|
+
try {
|
|
33
|
+
server.setRoute("post", "/", (req, res) => setting.selfRoom)
|
|
34
|
+
server.setRoute("get", "/", (req, res) => setting.selfRoom)
|
|
35
|
+
} catch (e) {
|
|
36
|
+
console.$error("initRoute failed", e)
|
|
37
|
+
}
|
|
38
|
+
_onRoom();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function _onRoom() {
|
|
43
|
+
setting.roomTid[0] = setTimeout(async () => {
|
|
44
|
+
try {
|
|
45
|
+
await initRoom();
|
|
46
|
+
} catch (e) {
|
|
47
|
+
console.$error('initRoom', e)
|
|
48
|
+
}
|
|
49
|
+
if (setting.roomTid[0]) {
|
|
50
|
+
_onRoom();
|
|
51
|
+
}
|
|
52
|
+
}, 3000)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function offRoom() {
|
|
56
|
+
if (setting.roomTid[0]) {
|
|
57
|
+
clearTimeout(setting.roomTid[0])
|
|
58
|
+
setting.roomTid[0] = null
|
|
59
|
+
if (setting.server) {
|
|
60
|
+
setting.server.close(() => {
|
|
61
|
+
console.$log('Existing server shut down.');
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getSelfIP(nodes) {
|
|
68
|
+
let ips = getLocalIPs().ipv4;
|
|
69
|
+
ips = ips.filter(i => nodes.indexOf(i) !== -1);
|
|
70
|
+
return ips[0]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function getTailscaleNodes() {
|
|
74
|
+
console.$log("getNodes")
|
|
75
|
+
let resp = await e(`${tailscalePath} status`);
|
|
76
|
+
let nodes = []
|
|
77
|
+
regEach(resp, /^(\d+\.\d+\.\d+\.\d+)\s+/mg, arr => {
|
|
78
|
+
nodes.push(arr[1])
|
|
79
|
+
});
|
|
80
|
+
return [...new Set(nodes)];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function enrichRoomInfo() {
|
|
84
|
+
let nodes = await getTailscaleNodes();
|
|
85
|
+
let ip = getSelfIP(nodes)
|
|
86
|
+
await fileJson(roomDataFile, async room => {
|
|
87
|
+
// 设置roomName
|
|
88
|
+
let name = os.hostname();
|
|
89
|
+
if (!vl(room.name) || room.name !== name) {
|
|
90
|
+
room.name = name;
|
|
91
|
+
console.$log("set roomName", name)
|
|
92
|
+
}
|
|
93
|
+
room.selfNode = ip;
|
|
94
|
+
room.nodes = nodes;
|
|
95
|
+
room.lastUpdateTime = Date.now()
|
|
96
|
+
console.$log("init room", room.name)
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function syncRooms() {
|
|
101
|
+
console.$log('syncRooms')
|
|
102
|
+
let roomsDir = getRoomsDir();
|
|
103
|
+
let room = await fileJson(roomDataFile)
|
|
104
|
+
|
|
105
|
+
let syncRooms = []
|
|
106
|
+
for (let node of room.nodes) {
|
|
107
|
+
if (node === room.selfNode) {
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
let respBody = await reqNode(node, "get", "/");
|
|
112
|
+
syncRooms.push(JSON.parse(respBody))
|
|
113
|
+
} catch (e) {
|
|
114
|
+
console.$log(`sync ${node} failed`, e);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
await fileLock(roomsDirLockKey, async () => {
|
|
118
|
+
for (let syncRoom of syncRooms) {
|
|
119
|
+
if (syncRoom.selfNode) {
|
|
120
|
+
await fp.writeFile(syncRoom.selfNode, JSON.stringify(syncRoom, null, 2))
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// 清理 roomsDir
|
|
126
|
+
let files = await fp.readdir(roomsDir)
|
|
127
|
+
let nodes = room.nodes;
|
|
128
|
+
for (let file of files) {
|
|
129
|
+
if (nodes.indexOf(file) === -1) {
|
|
130
|
+
try {
|
|
131
|
+
await fp.unlink(roomsDir + '/' + file)
|
|
132
|
+
} catch (e) {
|
|
133
|
+
console.$error(e);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function syncSetting() {
|
|
140
|
+
let room = await fileJson(roomDataFile)
|
|
141
|
+
await fileLock(roomsDirLockKey, async () => await _syncSetting(room));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function _syncSetting(room) {
|
|
145
|
+
setting.selfRoom = room;
|
|
146
|
+
setting.selfRoom.local = true;
|
|
147
|
+
let roomDir = getRoomsDir();
|
|
148
|
+
let rooms = []
|
|
149
|
+
let files = await fp.readdir(roomDir);
|
|
150
|
+
for (let node of setting.selfRoom.nodes) {
|
|
151
|
+
if (files.indexOf(node) !== -1) {
|
|
152
|
+
let resp = String(await fp.readFile(roomDir + '/' + node));
|
|
153
|
+
let room = JSON.parse(resp);
|
|
154
|
+
if (room.selfNode !== setting.selfRoom.selfNode) {
|
|
155
|
+
rooms.push(room)
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
rooms.push({
|
|
159
|
+
selfNode: node,
|
|
160
|
+
jsirs: []
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
setting.rooms = [setting.selfRoom, ...rooms]
|
|
165
|
+
.map(room => {
|
|
166
|
+
room.active = Date.now() - (room.lastUpdateTime || 0) <= 18000;
|
|
167
|
+
Object.entries(room.jsirs).forEach(([key, jsir]) =>
|
|
168
|
+
jsir.active = Date.now() - (jsir.lastUpdateTime || 0) <= 6000)
|
|
169
|
+
return room;
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function initRoom() {
|
|
174
|
+
let roomUpdateTouchTime = false;
|
|
175
|
+
await fileJson(roomDataFile, async room => {
|
|
176
|
+
await initRoomJsir(room)
|
|
177
|
+
if (!vl(room.lastUpdateTime) || (Date.now() - room.lastUpdateTime) > 9000) {
|
|
178
|
+
roomUpdateTouchTime = true;
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
if (roomUpdateTouchTime) {
|
|
183
|
+
await fileLock(enrichRoomInfoLockKey, enrichRoomInfo, false)
|
|
184
|
+
await fileLock(syncRoomsLockKey, syncRooms, false);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
await syncSetting();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function getEventLoopDelay() {
|
|
191
|
+
const start = process.hrtime.bigint();
|
|
192
|
+
const delay = 1; // 用于测量的短暂延迟(单位:毫秒)
|
|
193
|
+
const timeoutPromise = new Promise(resolve => setTimeout(resolve, delay));
|
|
194
|
+
return await timeoutPromise.then(() => {
|
|
195
|
+
const end = process.hrtime.bigint();
|
|
196
|
+
return Number(Math.max(Number(end - start) / 1e4 - (delay * 1e2), 0).toFixed(0)); // 避免负值
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function initRoomJsir(room) {
|
|
201
|
+
room.jsirs = Object.fromEntries(
|
|
202
|
+
Object.entries(room.jsirs || {}).filter(([pid]) => isPidAlive(pid))
|
|
203
|
+
);
|
|
204
|
+
room.jsirs[process.pid] = {
|
|
205
|
+
pid: process.pid,
|
|
206
|
+
space: setting.defaultSpace,
|
|
207
|
+
tips: getKeyTips().reduce((obj, key, index) => {
|
|
208
|
+
obj[key] = getValTips()[index];
|
|
209
|
+
return obj;
|
|
210
|
+
}, {}),
|
|
211
|
+
busy: await getEventLoopDelay(),
|
|
212
|
+
back: isRunningInBackground(),
|
|
213
|
+
lastUpdateTime: Date.now(),
|
|
214
|
+
port: setting.server.address().port
|
|
215
|
+
}
|
|
216
|
+
console.$log("init jsir", process.pid)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function isRunningInBackground() {
|
|
220
|
+
return !(process.stdout.isTTY && process.stdin.isTTY);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 获取本机所有内网 IP
|
|
225
|
+
* @returns {{ ipv4: string[], ipv6: string[] }}
|
|
226
|
+
*/
|
|
227
|
+
function getLocalIPs() {
|
|
228
|
+
const networkInterfaces = os.networkInterfaces();
|
|
229
|
+
|
|
230
|
+
const result = {
|
|
231
|
+
ipv4: [],
|
|
232
|
+
ipv6: []
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
for (const interfaceName of Object.keys(networkInterfaces)) {
|
|
236
|
+
for (const netInfo of networkInterfaces[interfaceName]) {
|
|
237
|
+
const { family, address, internal } = netInfo;
|
|
238
|
+
|
|
239
|
+
// 只关心非回环地址
|
|
240
|
+
if (!internal) {
|
|
241
|
+
if (family === 'IPv4') {
|
|
242
|
+
result.ipv4.push(address);
|
|
243
|
+
} else if (family === 'IPv6') {
|
|
244
|
+
result.ipv6.push(address);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function sshEnables(ips) {
|
|
254
|
+
// 检查开放 22 端口的 IP
|
|
255
|
+
const sshEnabledIPs = [];
|
|
256
|
+
const portCheckPromises = ips.map((ip) =>
|
|
257
|
+
checkPortOpen(ip, 22).then((isOpen) => {
|
|
258
|
+
if (isOpen) {
|
|
259
|
+
sshEnabledIPs.push(ip);
|
|
260
|
+
}
|
|
261
|
+
})
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
await Promise.all(portCheckPromises);
|
|
265
|
+
return sshEnabledIPs;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* 检查指定 IP 的指定端口是否开放
|
|
270
|
+
* @param {string} ip - 目标 IP 地址
|
|
271
|
+
* @param {number} port - 目标端口
|
|
272
|
+
* @returns {Promise<boolean>} - 是否开放
|
|
273
|
+
*/
|
|
274
|
+
function checkPortOpen(ip, port) {
|
|
275
|
+
return new Promise((resolve) => {
|
|
276
|
+
const socket = new net.Socket();
|
|
277
|
+
socket.setTimeout(1000); // 设置超时时间
|
|
278
|
+
|
|
279
|
+
socket
|
|
280
|
+
.connect(port, ip, () => {
|
|
281
|
+
socket.destroy(); // 成功连接后关闭 socket
|
|
282
|
+
resolve(true);
|
|
283
|
+
})
|
|
284
|
+
.on("error", () => {
|
|
285
|
+
socket.destroy(); // 遇到错误时关闭 socket
|
|
286
|
+
resolve(false);
|
|
287
|
+
})
|
|
288
|
+
.on("timeout", () => {
|
|
289
|
+
socket.destroy(); // 超时时关闭 socket
|
|
290
|
+
resolve(false);
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function reqNode(node, method, url) {
|
|
296
|
+
let port = 52108;
|
|
297
|
+
let room = setting.rooms.filter(i => i.selfNode === node)[0]
|
|
298
|
+
if (room) {
|
|
299
|
+
let activeJsir = Object.values(room.jsirs)
|
|
300
|
+
.filter(i => i.active).sort((a,b) => a.busy - b.busy);
|
|
301
|
+
if (activeJsir.length > 0) {
|
|
302
|
+
port = activeJsir[0].port;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
let opt = {
|
|
306
|
+
hostname: node,
|
|
307
|
+
port: port, // HTTP 默认端口
|
|
308
|
+
path: url,
|
|
309
|
+
method: method.toUpperCase(),
|
|
310
|
+
};
|
|
311
|
+
console.$log('reqRoom', JSON.stringify(opt))
|
|
312
|
+
return await new Promise((resolve, reject) => {
|
|
313
|
+
const req = http.request(opt, (res) => {
|
|
314
|
+
let data = '';
|
|
315
|
+
// 数据块接收
|
|
316
|
+
res.on('data', (chunk) => {
|
|
317
|
+
data += chunk;
|
|
318
|
+
});
|
|
319
|
+
// 响应结束
|
|
320
|
+
res.on('end', () => {
|
|
321
|
+
resolve(data)
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// 错误处理
|
|
326
|
+
req.on('error', (e) => {
|
|
327
|
+
reject(e)
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// 结束请求
|
|
331
|
+
req.end();
|
|
332
|
+
})
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
module.exports = {
|
|
336
|
+
onRoom,
|
|
337
|
+
offRoom,
|
|
338
|
+
syncSetting
|
|
339
|
+
}
|
package/deps/server.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
const {createConsole, vl} = require('./util');
|
|
3
|
+
const console = createConsole();
|
|
4
|
+
const setting = require('../deps/setting')
|
|
5
|
+
|
|
6
|
+
// 尝试监听的端口
|
|
7
|
+
const preferredPort = 52108;
|
|
8
|
+
|
|
9
|
+
// 路由存储
|
|
10
|
+
const routes = {};
|
|
11
|
+
|
|
12
|
+
// 创建一个 HTTP 服务
|
|
13
|
+
function createServer(port) {
|
|
14
|
+
const server = http.createServer(async (req, res) => {
|
|
15
|
+
const method = req.method.toLowerCase();
|
|
16
|
+
const url = req.url;
|
|
17
|
+
|
|
18
|
+
// 查找对应的路由处理函数
|
|
19
|
+
const routeKey = `${method} ${url}`;
|
|
20
|
+
if (routes[routeKey]) {
|
|
21
|
+
await routes[routeKey](req, res);
|
|
22
|
+
} else {
|
|
23
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
24
|
+
res.end('Not Found\n');
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
server.listen(port, () => {
|
|
29
|
+
const address = server.address();
|
|
30
|
+
console.$log(`Server listening on port ${address.port}`);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
server.on('error', (err) => {
|
|
34
|
+
console.$error(`Error occurred: ${err.message}`);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return server;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 尝试启动服务的逻辑
|
|
41
|
+
function startServer() {
|
|
42
|
+
if (!setting.server) {
|
|
43
|
+
try {
|
|
44
|
+
setting.server = createServer(preferredPort);
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.$error("startServer failed", e)
|
|
47
|
+
setting.server = createServer(0);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 设置路由的方法
|
|
53
|
+
function setRoute(method, url, fn, handler = jsonHandle) {
|
|
54
|
+
startServer();
|
|
55
|
+
|
|
56
|
+
const routeKey = `${method.toLowerCase()} ${url}`;
|
|
57
|
+
if (handler) {
|
|
58
|
+
routes[routeKey] = async (req, res) => {
|
|
59
|
+
await handler(req, res, fn)
|
|
60
|
+
};
|
|
61
|
+
} else {
|
|
62
|
+
routes[routeKey] = fn;
|
|
63
|
+
}
|
|
64
|
+
console.$log(`AddRoute: ${method.toUpperCase()} ${url} ${handler ? handler.constructor.name:''}`);
|
|
65
|
+
return setting.server;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function delRoute(method, url) {
|
|
69
|
+
const routeKey = `${method.toLowerCase()} ${url}`;
|
|
70
|
+
delete routes[routeKey];
|
|
71
|
+
console.$log(`DelRoute: ${method.toUpperCase()} ${url}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function jsonHandle(req, res, fn) {
|
|
75
|
+
let body = '';
|
|
76
|
+
|
|
77
|
+
// 监听请求数据
|
|
78
|
+
req.on('data', chunk => {
|
|
79
|
+
body += chunk;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
req.on('end', async () => {
|
|
83
|
+
try {
|
|
84
|
+
// 解析 JSON 请求体
|
|
85
|
+
const requestData = body ? JSON.parse(body):{};
|
|
86
|
+
|
|
87
|
+
// 创建响应 JSON 数据
|
|
88
|
+
let responseData = {};
|
|
89
|
+
let result = await fn(requestData, responseData);
|
|
90
|
+
if (vl(result)) {
|
|
91
|
+
responseData = result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 设置响应头
|
|
95
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
96
|
+
// 返回 JSON 响应
|
|
97
|
+
res.end(JSON.stringify(responseData));
|
|
98
|
+
} catch (error) {
|
|
99
|
+
// 错误处理:如果 JSON 解析失败
|
|
100
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
101
|
+
res.end(JSON.stringify({ error: 'Invalid JSON format' }));
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
setRoute,
|
|
108
|
+
delRoute,
|
|
109
|
+
jsonHandle
|
|
110
|
+
};
|
package/deps/setting.js
CHANGED
|
@@ -18,10 +18,13 @@ module.exports = {
|
|
|
18
18
|
initKey,
|
|
19
19
|
fileKey,
|
|
20
20
|
defaultType,
|
|
21
|
-
sigExit: true,
|
|
22
21
|
packages: {},
|
|
23
|
-
fileLock: {},
|
|
24
22
|
tips: {},
|
|
25
23
|
defaultSpace: 'local',
|
|
26
|
-
workspaceMap: {}
|
|
24
|
+
workspaceMap: {},
|
|
25
|
+
enableNextLine: true,
|
|
26
|
+
roomTid: [],
|
|
27
|
+
selfRoom: {},
|
|
28
|
+
rooms: [],
|
|
29
|
+
server: null
|
|
27
30
|
}
|
package/deps/util.js
CHANGED
|
@@ -23,6 +23,8 @@ let lockDir;
|
|
|
23
23
|
let logDir;
|
|
24
24
|
let tempDir;
|
|
25
25
|
let configDir;
|
|
26
|
+
let roomsDir;
|
|
27
|
+
let dataDir;
|
|
26
28
|
|
|
27
29
|
class SyncQueue {
|
|
28
30
|
constructor() {
|
|
@@ -708,7 +710,7 @@ function getInitName(fileName) {
|
|
|
708
710
|
}
|
|
709
711
|
|
|
710
712
|
function dataFile(fileName, fn, fmt, defaultObj = {}, returnStr = false) {
|
|
711
|
-
let dataDir =
|
|
713
|
+
let dataDir = getDataDir()
|
|
712
714
|
fileName = trim(fileName)
|
|
713
715
|
let path
|
|
714
716
|
if (fileName.startsWith("/")) {
|
|
@@ -758,11 +760,11 @@ async function fileExist(path) {
|
|
|
758
760
|
}
|
|
759
761
|
}
|
|
760
762
|
|
|
761
|
-
async function fileJson(key, fn, fmt = true) {
|
|
763
|
+
async function fileJson(key, fn = null, fmt = true, safeMs = 49000) {
|
|
762
764
|
`
|
|
763
765
|
多进程安全文件读写
|
|
764
766
|
`
|
|
765
|
-
let dataDir =
|
|
767
|
+
let dataDir = getDataDir()
|
|
766
768
|
let fileName = trim(key)
|
|
767
769
|
let path = dataDir + "/" + fileName;
|
|
768
770
|
let homeDir = setting.workspaceMap[setting.defaultSpace]
|
|
@@ -789,7 +791,7 @@ async function fileJson(key, fn, fmt = true) {
|
|
|
789
791
|
result = val
|
|
790
792
|
}
|
|
791
793
|
await fp.writeFile(path, prefixStr + JSON.stringify(result, null, fmt ? 2:null))
|
|
792
|
-
});
|
|
794
|
+
}, true, safeMs);
|
|
793
795
|
return result;
|
|
794
796
|
}
|
|
795
797
|
|
|
@@ -901,6 +903,24 @@ function getConfigDir() {
|
|
|
901
903
|
return configDir;
|
|
902
904
|
}
|
|
903
905
|
|
|
906
|
+
function getRoomsDir() {
|
|
907
|
+
if (roomsDir) {
|
|
908
|
+
return roomsDir
|
|
909
|
+
}
|
|
910
|
+
roomsDir = getLibDataDir() + '/rooms';
|
|
911
|
+
mkdir(roomsDir);
|
|
912
|
+
return roomsDir;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function getDataDir() {
|
|
916
|
+
if (dataDir) {
|
|
917
|
+
return dataDir
|
|
918
|
+
}
|
|
919
|
+
dataDir = getLibDataDir() + '/data';
|
|
920
|
+
mkdir(dataDir);
|
|
921
|
+
return dataDir;
|
|
922
|
+
}
|
|
923
|
+
|
|
904
924
|
function getAlias(aliasMap, key) {
|
|
905
925
|
key = trim(key)
|
|
906
926
|
if (!key) {
|
|
@@ -1037,7 +1057,7 @@ function getLockDir() {
|
|
|
1037
1057
|
return lockDir;
|
|
1038
1058
|
}
|
|
1039
1059
|
|
|
1040
|
-
function
|
|
1060
|
+
function getLockKeyDir(key) {
|
|
1041
1061
|
if (!key) {
|
|
1042
1062
|
throw "invalid args"
|
|
1043
1063
|
}
|
|
@@ -1046,36 +1066,110 @@ function getLockFile(key) {
|
|
|
1046
1066
|
return lockDir + "/" + key;
|
|
1047
1067
|
}
|
|
1048
1068
|
|
|
1049
|
-
|
|
1069
|
+
/**
|
|
1070
|
+
* 从目录中读取过期时间戳
|
|
1071
|
+
* @param {string} lockDir - 锁目录路径
|
|
1072
|
+
* @returns {Promise<number | null>} 如果目录下存在过期时间戳文件,则返回其数值;否则返回 null
|
|
1073
|
+
*/
|
|
1074
|
+
async function readExpireTimestamp(lockDir) {
|
|
1075
|
+
try {
|
|
1076
|
+
const files = await fp.readdir(lockDir);
|
|
1077
|
+
// 假设我们只会创建一个文件,其文件名包含过期时间,如 `expire-1672531200000`
|
|
1078
|
+
const expireFile = files.find(name => name.startsWith('expire-'));
|
|
1079
|
+
if (!expireFile) {
|
|
1080
|
+
return null;
|
|
1081
|
+
}
|
|
1082
|
+
const timestamp = parseInt(expireFile.replace('expire-', ''), 10);
|
|
1083
|
+
return isNaN(timestamp) ? null : timestamp;
|
|
1084
|
+
} catch (err) {
|
|
1085
|
+
// 读取目录出错(可能不存在?)直接返回 null
|
|
1086
|
+
return null;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
async function _fileLock(key, fn, expireMs = 49000) {
|
|
1050
1091
|
`
|
|
1051
1092
|
文件锁,返回true/false,
|
|
1052
1093
|
如果没锁住则不执行fn
|
|
1053
1094
|
`
|
|
1054
|
-
if (!key ||
|
|
1055
|
-
throw new Error('invalid
|
|
1095
|
+
if (!key || typeof fn !== 'function') {
|
|
1096
|
+
throw new Error('invalid arguments');
|
|
1056
1097
|
}
|
|
1057
|
-
|
|
1098
|
+
|
|
1099
|
+
let lockKeyDir = getLockKeyDir(key)
|
|
1100
|
+
const now = Date.now();
|
|
1101
|
+
const expireAt = expireMs > 0 ? now + expireMs : 0;
|
|
1102
|
+
|
|
1103
|
+
// 1. 尝试判断锁目录是否已存在
|
|
1104
|
+
let lockExists = false;
|
|
1058
1105
|
try {
|
|
1059
|
-
await fp.
|
|
1060
|
-
|
|
1106
|
+
await fp.access(lockKeyDir); // 若存在则不抛错
|
|
1107
|
+
lockExists = true;
|
|
1108
|
+
} catch (_) {
|
|
1109
|
+
// 不存在时会抛ENOENT错误,说明还没有锁
|
|
1110
|
+
lockExists = false;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
if (lockExists) {
|
|
1114
|
+
// 目录存在时,读取文件名中的过期时间戳
|
|
1115
|
+
const storedExpireAt = await readExpireTimestamp(lockKeyDir);
|
|
1116
|
+
if (storedExpireAt === null) {
|
|
1117
|
+
// 理论上不会出现没有“expire-xxxx”文件的情况,除非中途被手动改动
|
|
1118
|
+
// 可以选择强制删除目录,或者直接返回 false
|
|
1119
|
+
return false;
|
|
1120
|
+
}
|
|
1121
|
+
if (storedExpireAt === 0) {
|
|
1122
|
+
// 说明当时设置的是永不过期
|
|
1123
|
+
return false;
|
|
1124
|
+
}
|
|
1125
|
+
if (storedExpireAt > now) {
|
|
1126
|
+
// 说明锁尚未过期
|
|
1127
|
+
return false;
|
|
1128
|
+
}
|
|
1129
|
+
// 如果过期了,则清理原目录,以便重新加锁
|
|
1130
|
+
try {
|
|
1131
|
+
await fp.rm(lockKeyDir, { recursive: true, force: true });
|
|
1132
|
+
} catch (err) {
|
|
1133
|
+
// 删除失败,可能是权限问题;这里直接返回 false 或者抛错
|
|
1134
|
+
return false;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// 2. 创建锁目录
|
|
1139
|
+
try {
|
|
1140
|
+
await fp.mkdir(lockKeyDir, { recursive: false });
|
|
1141
|
+
} catch (err) {
|
|
1142
|
+
// 如果 mkdir 依旧失败,说明在这段时间里又被别人抢先创建了
|
|
1143
|
+
return false;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// 3. 在锁目录下创建一个文件,文件名带“过期时间戳”
|
|
1147
|
+
// expireAt === 0 表示永不过期
|
|
1148
|
+
const expireFilename = `expire-${expireAt}`;
|
|
1149
|
+
const expireFilePath = path.join(lockKeyDir, expireFilename);
|
|
1150
|
+
try {
|
|
1151
|
+
await fp.writeFile(expireFilePath, '');
|
|
1152
|
+
} catch (err) {
|
|
1153
|
+
// 无法写文件就释放锁并抛错
|
|
1154
|
+
try {
|
|
1155
|
+
await fp.rm(lockKeyDir, { recursive: true, force: true });
|
|
1156
|
+
} catch (_) {}
|
|
1061
1157
|
return false;
|
|
1062
1158
|
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1159
|
+
|
|
1160
|
+
// 4. 成功加锁后执行 fn
|
|
1065
1161
|
try {
|
|
1066
1162
|
await fn();
|
|
1067
1163
|
return true;
|
|
1068
|
-
} catch (e) {
|
|
1069
|
-
throw e;
|
|
1070
1164
|
} finally {
|
|
1165
|
+
// 5. 执行完毕后,手动释放锁目录(也可选择保留到过期自动失效,但一般都建议主动释放)
|
|
1071
1166
|
try {
|
|
1072
|
-
await fp.
|
|
1073
|
-
|
|
1074
|
-
} catch (_){}
|
|
1167
|
+
await fp.rm(lockKeyDir, { recursive: true, force: true });
|
|
1168
|
+
} catch (_) {}
|
|
1075
1169
|
}
|
|
1076
1170
|
}
|
|
1077
1171
|
|
|
1078
|
-
async function fileLock(key, fn, wait = true) {
|
|
1172
|
+
async function fileLock(key, fn, wait = true, expireMs = 49000) {
|
|
1079
1173
|
`
|
|
1080
1174
|
文件锁, 默认一直等待,直到加锁成功执行fn
|
|
1081
1175
|
wait = false, 加锁失败则不执行fn
|
|
@@ -1083,13 +1177,13 @@ async function fileLock(key, fn, wait = true) {
|
|
|
1083
1177
|
`
|
|
1084
1178
|
if (wait) {
|
|
1085
1179
|
while (true) {
|
|
1086
|
-
if (await _fileLock(key, fn)) {
|
|
1180
|
+
if (await _fileLock(key, fn, expireMs)) {
|
|
1087
1181
|
break;
|
|
1088
1182
|
}
|
|
1089
|
-
await sleep(
|
|
1183
|
+
await sleep(9);
|
|
1090
1184
|
}
|
|
1091
1185
|
} else {
|
|
1092
|
-
await _fileLock(key, fn);
|
|
1186
|
+
await _fileLock(key, fn, expireMs);
|
|
1093
1187
|
}
|
|
1094
1188
|
}
|
|
1095
1189
|
|
|
@@ -1395,17 +1489,17 @@ async function eia(cmd, args = [], shell = false) {
|
|
|
1395
1489
|
`
|
|
1396
1490
|
当前进程不会卡住,输入输出由cmd进程持有
|
|
1397
1491
|
`
|
|
1398
|
-
setting.
|
|
1492
|
+
setting.enableNextLine = false;
|
|
1399
1493
|
let child = spawn(cmd, args, {stdio:"inherit", shell});
|
|
1400
1494
|
return new Promise((resolve, reject) => {
|
|
1401
1495
|
// 监听子进程的关闭事件
|
|
1402
1496
|
child.on('close', (code) => {
|
|
1403
|
-
setting.
|
|
1497
|
+
setting.enableNextLine = true;
|
|
1404
1498
|
resolve(code);
|
|
1405
1499
|
});
|
|
1406
1500
|
// 可选:监听子进程的错误事件
|
|
1407
1501
|
child.on('error', (err) => {
|
|
1408
|
-
setting.
|
|
1502
|
+
setting.enableNextLine = true;
|
|
1409
1503
|
reject(err)
|
|
1410
1504
|
});
|
|
1411
1505
|
})
|
|
@@ -1863,6 +1957,29 @@ end tell
|
|
|
1863
1957
|
}
|
|
1864
1958
|
}
|
|
1865
1959
|
|
|
1960
|
+
function getKeyTips() {
|
|
1961
|
+
let items = [];
|
|
1962
|
+
for (let key of Object.keys(setting.tips)) {
|
|
1963
|
+
items.push(trim(key))
|
|
1964
|
+
}
|
|
1965
|
+
return items
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
function getValTips() {
|
|
1969
|
+
let items = [];
|
|
1970
|
+
for (let key of Object.keys(setting.tips)) {
|
|
1971
|
+
let val = setting.tips[key].map(i => {
|
|
1972
|
+
let item = trim(i)
|
|
1973
|
+
if (item.indexOf(',') !== -1) {
|
|
1974
|
+
item = `[${item}]`;
|
|
1975
|
+
}
|
|
1976
|
+
return item;
|
|
1977
|
+
}).join("|");
|
|
1978
|
+
items.push(val)
|
|
1979
|
+
}
|
|
1980
|
+
return items
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1866
1983
|
module.exports = {
|
|
1867
1984
|
wrapperJsirText,
|
|
1868
1985
|
run,
|
|
@@ -1963,5 +2080,9 @@ module.exports = {
|
|
|
1963
2080
|
createDirs,
|
|
1964
2081
|
getTempDir,
|
|
1965
2082
|
terminalRun,
|
|
1966
|
-
eia
|
|
2083
|
+
eia,
|
|
2084
|
+
getKeyTips,
|
|
2085
|
+
getValTips,
|
|
2086
|
+
getRoomsDir,
|
|
2087
|
+
getDataDir
|
|
1967
2088
|
}
|