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 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 items = [];
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 = getLibDataDir() + "/data"
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 = getLibDataDir() + "/data"
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 getLockFile(key) {
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
- async function _fileLock(key, fn) {
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 || !fn) {
1055
- throw new Error('invalid args')
1095
+ if (!key || typeof fn !== 'function') {
1096
+ throw new Error('invalid arguments');
1056
1097
  }
1057
- let file = getLockFile(key)
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.mkdir(file);
1060
- } catch (e) {
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
- let fileLockMap = setting.fileLock;
1064
- fileLockMap[file] = true;
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.rmdir(file)
1073
- delete fileLockMap[file]
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(49);
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.sigExit = false;
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.sigExit = true;
1497
+ setting.enableNextLine = true;
1404
1498
  resolve(code);
1405
1499
  });
1406
1500
  // 可选:监听子进程的错误事件
1407
1501
  child.on('error', (err) => {
1408
- setting.sigExit = true;
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsir",
3
- "version": "2.3.2",
3
+ "version": "2.3.4",
4
4
  "description": "JavaScript Script Management Tool",
5
5
  "main": "index.js",
6
6
  "scripts": {