jsir 2.3.2 → 2.3.3

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
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;
@@ -1357,6 +1342,8 @@ const keywordDef = {
1357
1342
  _noAppendNextLine = false
1358
1343
  resetCmdMap()
1359
1344
  console.log(warnStr(`(${setting.name} ${packageJson.version}) You can start with .help, use * to expand context.`))
1345
+ room.onRoom()
1346
+ nextLine()
1360
1347
  },
1361
1348
  short: 'p'
1362
1349
  },
@@ -2085,20 +2072,9 @@ async function evalText($text = '', $cmdName = '', $args = []) {
2085
2072
  $homeDir, $lib, _cmdMap);
2086
2073
  }
2087
2074
 
2088
- function clearFileLock() {
2089
- for (let file of Object.keys(fileLockMap)) {
2090
- fp.rmdir(file)
2091
- delete setting.fileLock[file]
2092
- }
2093
- }
2094
-
2095
2075
  function sigExit() {
2096
- if (!setting.sigExit) {
2097
- return;
2098
- }
2099
2076
  if (_noAppendNextLine) {
2100
2077
  delTips();
2101
- clearFileLock();
2102
2078
  process.exit(0);
2103
2079
  } else {
2104
2080
  nextLine();
@@ -2107,15 +2083,12 @@ function sigExit() {
2107
2083
 
2108
2084
  process.on('uncaughtException',function(err){
2109
2085
  console.$error('uncaughtException', err)
2110
- _noAppendNextLine || nextLine()
2111
2086
  })
2112
2087
  process.on('unhandledRejection',function(err){
2113
2088
  console.$error('unhandledRejection', err)
2114
- _noAppendNextLine || nextLine()
2115
2089
  })
2116
2090
  process.on('rejectionHandled',function(err){
2117
2091
  console.$error('rejectionHandled', err)
2118
- _noAppendNextLine || nextLine()
2119
2092
  })
2120
2093
  process.on('SIGINT', sigExit);
2121
2094
  process.on('SIGTERM', sigExit);
package/deps/room.js ADDED
@@ -0,0 +1,232 @@
1
+ const os = require('os');
2
+ const {fileJson, fileLock, vl, createConsole, getKeyTips, getValTips} = require('./util');
3
+ const roomDataFile = "jsirRoom.json"
4
+ const console = createConsole();
5
+ const ping = require("ping");
6
+ const net = require("net");
7
+ const setting = require('../deps/setting')
8
+
9
+ function isPidAlive(pid) {
10
+ try {
11
+ process.kill(Number(pid), 0); // 信号 0 不会实际发送,但会检查进程是否存在
12
+ return true; // PID 存在
13
+ } catch (err) {
14
+ if (err.code === 'ESRCH') {
15
+ return false; // 进程不存在
16
+ }
17
+ console.$error(`check isPidAlive ${pid} failed`, err)
18
+ return false;
19
+ }
20
+ }
21
+
22
+ function onRoom() {
23
+ return
24
+ if (!setting.roomTid[0]) {
25
+ _onRoom();
26
+ }
27
+ }
28
+
29
+ function _onRoom() {
30
+ setting.roomTid[0] = setTimeout(async () => {
31
+ try {
32
+ await _initRoom();
33
+ } catch (e) {
34
+ console.$error('initRoom', e)
35
+ }
36
+ if (setting.roomTid[0]) {
37
+ _onRoom();
38
+ }
39
+ }, 1000)
40
+ }
41
+
42
+ function offRoom() {
43
+ return;
44
+ if (setting.roomTid[0]) {
45
+ clearTimeout(setting.roomTid[0])
46
+ setting.roomTid[0] = null
47
+ }
48
+ }
49
+
50
+ async function getTailScaleNodes() {
51
+ console.$log("getNodes")
52
+ let ips = getLocalIPs().ipv4;
53
+ ips = ips.filter(i => i.startsWith("100.")); // tailScale ip前缀
54
+ let nodes = []
55
+ for (let ip of ips) {
56
+ let netIps = await scanLocalNetwork(ip);
57
+ netIps = netIps.filter(i => i !== ip)
58
+ nodes.push(...netIps)
59
+ }
60
+ return await sshEnables(nodes);
61
+ }
62
+
63
+ async function initRoomInfo() {
64
+ await fileLock(roomDataFile + "getNodes", async () => {
65
+ let nodes = await getTailScaleNodes();
66
+ await fileJson(roomDataFile, async room => {
67
+ // 设置roomName
68
+ let name = os.hostname();
69
+ if (!vl(room.name) || room.name !== name) {
70
+ room.name = name;
71
+ console.$log("set roomName", name)
72
+ }
73
+
74
+ room.nodes = nodes;
75
+ room.lastUpdateTime = Date.now()
76
+ console.$log("init room", room.name)
77
+ })
78
+ }, false)
79
+ }
80
+
81
+ async function _initRoom() {
82
+ let roomUpdateTouchTime = false;
83
+ await fileJson(roomDataFile, async room => {
84
+ await initRoomJsir(room)
85
+ if (!vl(room.lastUpdateTime) || (Date.now() - room.lastUpdateTime) > 9000) {
86
+ roomUpdateTouchTime = true;
87
+ }
88
+ })
89
+
90
+ if (roomUpdateTouchTime) {
91
+ initRoomInfo();
92
+ }
93
+ }
94
+
95
+ async function getEventLoopDelay() {
96
+ const start = process.hrtime.bigint();
97
+ const delay = 1; // 用于测量的短暂延迟(单位:毫秒)
98
+ const timeoutPromise = new Promise(resolve => setTimeout(resolve, delay));
99
+ return await timeoutPromise.then(() => {
100
+ const end = process.hrtime.bigint();
101
+ return Number(Math.max(Number(end - start) / 1e4 - (delay * 1e2), 0).toFixed(0)); // 避免负值
102
+ });
103
+ }
104
+
105
+ async function initRoomJsir(room) {
106
+ room.jsir = room.jsir || {};
107
+
108
+ for (let pid of [...Object.keys(room.jsir)]) {
109
+ if (!isPidAlive(pid)) {
110
+ delete room.jsir[pid]
111
+ }
112
+ }
113
+ room.jsir[process.pid] = {
114
+ pid: process.pid,
115
+ space: setting.defaultSpace,
116
+ tips: getKeyTips().reduce((obj, key, index) => {
117
+ obj[key] = getValTips()[index];
118
+ return obj;
119
+ }, {}),
120
+ busy: await getEventLoopDelay(),
121
+ back: isRunningInBackground(),
122
+ lastUpdateTime: Date.now()
123
+ }
124
+ console.$log("init jsir", process.pid)
125
+ }
126
+
127
+ function isRunningInBackground() {
128
+ return !(process.stdout.isTTY && process.stdin.isTTY);
129
+ }
130
+
131
+ /**
132
+ * 获取本机所有内网 IP
133
+ * @returns {{ ipv4: string[], ipv6: string[] }}
134
+ */
135
+ function getLocalIPs() {
136
+ const networkInterfaces = os.networkInterfaces();
137
+
138
+ const result = {
139
+ ipv4: [],
140
+ ipv6: []
141
+ };
142
+
143
+ for (const interfaceName of Object.keys(networkInterfaces)) {
144
+ for (const netInfo of networkInterfaces[interfaceName]) {
145
+ const { family, address, internal } = netInfo;
146
+
147
+ // 只关心非回环地址
148
+ if (!internal) {
149
+ if (family === 'IPv4') {
150
+ result.ipv4.push(address);
151
+ } else if (family === 'IPv6') {
152
+ result.ipv6.push(address);
153
+ }
154
+ }
155
+ }
156
+ }
157
+
158
+ return result;
159
+ }
160
+
161
+ async function sshEnables(ips) {
162
+ // 检查开放 22 端口的 IP
163
+ const sshEnabledIPs = [];
164
+ const portCheckPromises = ips.map((ip) =>
165
+ checkPortOpen(ip, 22).then((isOpen) => {
166
+ if (isOpen) {
167
+ sshEnabledIPs.push(ip);
168
+ }
169
+ })
170
+ );
171
+
172
+ await Promise.all(portCheckPromises);
173
+ return sshEnabledIPs;
174
+ }
175
+
176
+ /**
177
+ * 检查指定 IP 的指定端口是否开放
178
+ * @param {string} ip - 目标 IP 地址
179
+ * @param {number} port - 目标端口
180
+ * @returns {Promise<boolean>} - 是否开放
181
+ */
182
+ function checkPortOpen(ip, port) {
183
+ return new Promise((resolve) => {
184
+ const socket = new net.Socket();
185
+ socket.setTimeout(1000); // 设置超时时间
186
+
187
+ socket
188
+ .connect(port, ip, () => {
189
+ socket.destroy(); // 成功连接后关闭 socket
190
+ resolve(true);
191
+ })
192
+ .on("error", () => {
193
+ socket.destroy(); // 遇到错误时关闭 socket
194
+ resolve(false);
195
+ })
196
+ .on("timeout", () => {
197
+ socket.destroy(); // 超时时关闭 socket
198
+ resolve(false);
199
+ });
200
+ });
201
+ }
202
+
203
+ /**
204
+ * 扫描局域网中的可访问IP
205
+ * @param {string} localIP - 本机内网IP地址
206
+ * @returns {Promise<string[]>} - 可访问的IP地址列表
207
+ */
208
+ async function scanLocalNetwork(localIP) {
209
+ const subnet = localIP.substring(0, localIP.lastIndexOf('.') + 1); // 例如 "192.168.1."
210
+ const reachableIPs = [];
211
+
212
+ // 扫描 1 到 254 的 IP
213
+ const promises = [];
214
+ for (let i = 1; i <= 254; i++) {
215
+ const ip = `${subnet}${i}`;
216
+ promises.push(
217
+ ping.promise.probe(ip, { timeout: 1 }).then((res) => {
218
+ if (res.alive) {
219
+ reachableIPs.push(ip);
220
+ }
221
+ })
222
+ );
223
+ }
224
+
225
+ await Promise.all(promises);
226
+ return reachableIPs;
227
+ }
228
+
229
+ module.exports = {
230
+ onRoom,
231
+ offRoom
232
+ }
package/deps/setting.js CHANGED
@@ -18,10 +18,10 @@ 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
27
  }
package/deps/util.js CHANGED
@@ -758,7 +758,7 @@ async function fileExist(path) {
758
758
  }
759
759
  }
760
760
 
761
- async function fileJson(key, fn, fmt = true) {
761
+ async function fileJson(key, fn, fmt = true, safeMs = 49000) {
762
762
  `
763
763
  多进程安全文件读写
764
764
  `
@@ -789,7 +789,7 @@ async function fileJson(key, fn, fmt = true) {
789
789
  result = val
790
790
  }
791
791
  await fp.writeFile(path, prefixStr + JSON.stringify(result, null, fmt ? 2:null))
792
- });
792
+ }, true, safeMs);
793
793
  return result;
794
794
  }
795
795
 
@@ -1037,7 +1037,7 @@ function getLockDir() {
1037
1037
  return lockDir;
1038
1038
  }
1039
1039
 
1040
- function getLockFile(key) {
1040
+ function getLockKeyDir(key) {
1041
1041
  if (!key) {
1042
1042
  throw "invalid args"
1043
1043
  }
@@ -1046,36 +1046,110 @@ function getLockFile(key) {
1046
1046
  return lockDir + "/" + key;
1047
1047
  }
1048
1048
 
1049
- async function _fileLock(key, fn) {
1049
+ /**
1050
+ * 从目录中读取过期时间戳
1051
+ * @param {string} lockDir - 锁目录路径
1052
+ * @returns {Promise<number | null>} 如果目录下存在过期时间戳文件,则返回其数值;否则返回 null
1053
+ */
1054
+ async function readExpireTimestamp(lockDir) {
1055
+ try {
1056
+ const files = await fp.readdir(lockDir);
1057
+ // 假设我们只会创建一个文件,其文件名包含过期时间,如 `expire-1672531200000`
1058
+ const expireFile = files.find(name => name.startsWith('expire-'));
1059
+ if (!expireFile) {
1060
+ return null;
1061
+ }
1062
+ const timestamp = parseInt(expireFile.replace('expire-', ''), 10);
1063
+ return isNaN(timestamp) ? null : timestamp;
1064
+ } catch (err) {
1065
+ // 读取目录出错(可能不存在?)直接返回 null
1066
+ return null;
1067
+ }
1068
+ }
1069
+
1070
+ async function _fileLock(key, fn, expireMs = 49000) {
1050
1071
  `
1051
1072
  文件锁,返回true/false,
1052
1073
  如果没锁住则不执行fn
1053
1074
  `
1054
- if (!key || !fn) {
1055
- throw new Error('invalid args')
1075
+ if (!key || typeof fn !== 'function') {
1076
+ throw new Error('invalid arguments');
1056
1077
  }
1057
- let file = getLockFile(key)
1078
+
1079
+ let lockKeyDir = getLockKeyDir(key)
1080
+ const now = Date.now();
1081
+ const expireAt = expireMs > 0 ? now + expireMs : 0;
1082
+
1083
+ // 1. 尝试判断锁目录是否已存在
1084
+ let lockExists = false;
1058
1085
  try {
1059
- await fp.mkdir(file);
1060
- } catch (e) {
1086
+ await fp.access(lockKeyDir); // 若存在则不抛错
1087
+ lockExists = true;
1088
+ } catch (_) {
1089
+ // 不存在时会抛ENOENT错误,说明还没有锁
1090
+ lockExists = false;
1091
+ }
1092
+
1093
+ if (lockExists) {
1094
+ // 目录存在时,读取文件名中的过期时间戳
1095
+ const storedExpireAt = await readExpireTimestamp(lockKeyDir);
1096
+ if (storedExpireAt === null) {
1097
+ // 理论上不会出现没有“expire-xxxx”文件的情况,除非中途被手动改动
1098
+ // 可以选择强制删除目录,或者直接返回 false
1099
+ return false;
1100
+ }
1101
+ if (storedExpireAt === 0) {
1102
+ // 说明当时设置的是永不过期
1103
+ return false;
1104
+ }
1105
+ if (storedExpireAt > now) {
1106
+ // 说明锁尚未过期
1107
+ return false;
1108
+ }
1109
+ // 如果过期了,则清理原目录,以便重新加锁
1110
+ try {
1111
+ await fp.rm(lockKeyDir, { recursive: true, force: true });
1112
+ } catch (err) {
1113
+ // 删除失败,可能是权限问题;这里直接返回 false 或者抛错
1114
+ return false;
1115
+ }
1116
+ }
1117
+
1118
+ // 2. 创建锁目录
1119
+ try {
1120
+ await fp.mkdir(lockKeyDir, { recursive: false });
1121
+ } catch (err) {
1122
+ // 如果 mkdir 依旧失败,说明在这段时间里又被别人抢先创建了
1061
1123
  return false;
1062
1124
  }
1063
- let fileLockMap = setting.fileLock;
1064
- fileLockMap[file] = true;
1125
+
1126
+ // 3. 在锁目录下创建一个文件,文件名带“过期时间戳”
1127
+ // expireAt === 0 表示永不过期
1128
+ const expireFilename = `expire-${expireAt}`;
1129
+ const expireFilePath = path.join(lockKeyDir, expireFilename);
1130
+ try {
1131
+ await fp.writeFile(expireFilePath, '');
1132
+ } catch (err) {
1133
+ // 无法写文件就释放锁并抛错
1134
+ try {
1135
+ await fp.rm(lockKeyDir, { recursive: true, force: true });
1136
+ } catch (_) {}
1137
+ return false;
1138
+ }
1139
+
1140
+ // 4. 成功加锁后执行 fn
1065
1141
  try {
1066
1142
  await fn();
1067
1143
  return true;
1068
- } catch (e) {
1069
- throw e;
1070
1144
  } finally {
1145
+ // 5. 执行完毕后,手动释放锁目录(也可选择保留到过期自动失效,但一般都建议主动释放)
1071
1146
  try {
1072
- await fp.rmdir(file)
1073
- delete fileLockMap[file]
1074
- } catch (_){}
1147
+ await fp.rm(lockKeyDir, { recursive: true, force: true });
1148
+ } catch (_) {}
1075
1149
  }
1076
1150
  }
1077
1151
 
1078
- async function fileLock(key, fn, wait = true) {
1152
+ async function fileLock(key, fn, wait = true, expireMs = 49000) {
1079
1153
  `
1080
1154
  文件锁, 默认一直等待,直到加锁成功执行fn
1081
1155
  wait = false, 加锁失败则不执行fn
@@ -1083,13 +1157,13 @@ async function fileLock(key, fn, wait = true) {
1083
1157
  `
1084
1158
  if (wait) {
1085
1159
  while (true) {
1086
- if (await _fileLock(key, fn)) {
1160
+ if (await _fileLock(key, fn, expireMs)) {
1087
1161
  break;
1088
1162
  }
1089
- await sleep(49);
1163
+ await sleep(9);
1090
1164
  }
1091
1165
  } else {
1092
- await _fileLock(key, fn);
1166
+ await _fileLock(key, fn, expireMs);
1093
1167
  }
1094
1168
  }
1095
1169
 
@@ -1395,17 +1469,17 @@ async function eia(cmd, args = [], shell = false) {
1395
1469
  `
1396
1470
  当前进程不会卡住,输入输出由cmd进程持有
1397
1471
  `
1398
- setting.sigExit = false;
1472
+ setting.enableNextLine = false;
1399
1473
  let child = spawn(cmd, args, {stdio:"inherit", shell});
1400
1474
  return new Promise((resolve, reject) => {
1401
1475
  // 监听子进程的关闭事件
1402
1476
  child.on('close', (code) => {
1403
- setting.sigExit = true;
1477
+ setting.enableNextLine = true;
1404
1478
  resolve(code);
1405
1479
  });
1406
1480
  // 可选:监听子进程的错误事件
1407
1481
  child.on('error', (err) => {
1408
- setting.sigExit = true;
1482
+ setting.enableNextLine = true;
1409
1483
  reject(err)
1410
1484
  });
1411
1485
  })
@@ -1863,6 +1937,29 @@ end tell
1863
1937
  }
1864
1938
  }
1865
1939
 
1940
+ function getKeyTips() {
1941
+ let items = [];
1942
+ for (let key of Object.keys(setting.tips)) {
1943
+ items.push(trim(key))
1944
+ }
1945
+ return items
1946
+ }
1947
+
1948
+ function getValTips() {
1949
+ let items = [];
1950
+ for (let key of Object.keys(setting.tips)) {
1951
+ let val = setting.tips[key].map(i => {
1952
+ let item = trim(i)
1953
+ if (item.indexOf(',') !== -1) {
1954
+ item = `[${item}]`;
1955
+ }
1956
+ return item;
1957
+ }).join("|");
1958
+ items.push(val)
1959
+ }
1960
+ return items
1961
+ }
1962
+
1866
1963
  module.exports = {
1867
1964
  wrapperJsirText,
1868
1965
  run,
@@ -1963,5 +2060,7 @@ module.exports = {
1963
2060
  createDirs,
1964
2061
  getTempDir,
1965
2062
  terminalRun,
1966
- eia
2063
+ eia,
2064
+ getKeyTips,
2065
+ getValTips
1967
2066
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsir",
3
- "version": "2.3.2",
3
+ "version": "2.3.3",
4
4
  "description": "JavaScript Script Management Tool",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -22,6 +22,7 @@
22
22
  "chokidar": "^3.5.2",
23
23
  "console.table": "^0.10.0",
24
24
  "dayjs": "^1.10.4",
25
- "pad": "^3.2.0"
25
+ "pad": "^3.2.0",
26
+ "ping": "^0.4.4"
26
27
  }
27
28
  }