jsir 3.1.1 → 3.1.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
@@ -13,7 +13,7 @@ const {
13
13
  getFullPath, parseUniqueName, toUniqueName, isJsirFileName, toJsirFileName,
14
14
  getAlias, wrapperJsirText, eia, getKeyTips, getValTips, getJsirTypeKey,
15
15
  createDetachedProcess, interceptStdStreams,
16
- draftModify, isRunningInBackground, fileJson, fileLock, processLock, cleanFileLocks, getMd5Key, terminalTitle,
16
+ draftModify, isRunningInBackground, fileJson, fileLock, processLock, getMd5Key, terminalTitle,
17
17
  getFileOpenExe, formatSec, formatMb
18
18
  } = $lib;
19
19
  const _args = process.argv.slice(2).map(trim);
@@ -150,7 +150,7 @@ const $data = {
150
150
  return _dealOnLazyGet(key, onLazyTempCode);
151
151
  }
152
152
  let result = null;
153
- await processLock('gafs:' + key, async () => {
153
+ await processLock('jsir:gafs:' + key, async () => {
154
154
  if (!vl(_data[key])) {
155
155
  result = _dataSet(key, await fn(), onLazyTempCode)
156
156
  } else {
@@ -203,7 +203,6 @@ function checkWorkspaces() {
203
203
 
204
204
  async function start() {
205
205
  terminalTitle()
206
- cleanFileLocks();
207
206
 
208
207
  setting.wrapperInput = wrapperInput;
209
208
  interceptStdStreams()
package/deps/room.js CHANGED
@@ -2,8 +2,8 @@ const os = require('os');
2
2
  const {fileJson, fileLock, vl, getKeyTips, getValTips,
3
3
  getRoomsDir, e, isRunningInBackground,
4
4
  batchAsync, trim, getOr, errorTag, warnStr, getConfig, getConfigDir,
5
- fileExist, processLock, isPidAlive, cleanFileLocks,
6
- roomConsole: console, reget, md5
5
+ fileExist, processLock, isPidAlive, getLockDir,
6
+ roomConsole: console, reget, md5, cacheFn
7
7
  } = require('./util');
8
8
  const {setRoute, createSign} = require('../deps/server')
9
9
  const roomDataFile = "jsirRoom.json"
@@ -17,6 +17,8 @@ const http = require('http');
17
17
  const tailscalePath = os.platform() === 'darwin' ?
18
18
  '/Applications/Tailscale.app/Contents/MacOS/Tailscale':'tailscale';
19
19
  const packageJson = require("../package.json");
20
+ const { performance } = require('perf_hooks');
21
+ let elu0 = performance.eventLoopUtilization()
20
22
 
21
23
  async function localConfigs(uniqueNameList) {
22
24
  if (uniqueNameList && uniqueNameList.length > 0 ) {
@@ -338,10 +340,58 @@ async function processTasks(defTasks) {
338
340
  })
339
341
  }
340
342
 
341
- async function initRoom() {
342
- if (process.pid === setting.defaultPort) {
343
- cleanFileLocks();
343
+ function cleanFnCache() {
344
+ const now = Date.now()
345
+ for (let key of Object.keys(setting.fnCache)) {
346
+ let item = setting.fnCache[key]
347
+ if (item && item.validMsTime && now > item.validMsTime) {
348
+ delete setting.fnCache[key]
349
+ }
350
+ }
351
+ console.$debug("cleanFnCache cost", `${Date.now() - now}ms`)
352
+ }
353
+
354
+ async function readLockPid(lockDir) {
355
+ try {
356
+ const files = await fp.readdir(lockDir);
357
+ const pidFile = files.find(name => name.startsWith('pid-'));
358
+ if (!pidFile) {
359
+ return null;
360
+ }
361
+ return pidFile.split("-")[1];
362
+ } catch (err) {
363
+ // 读取目录出错(可能不存在?)直接返回 null
364
+ return null;
344
365
  }
366
+ }
367
+
368
+ async function cleanFileLocks() {
369
+ const now = Date.now()
370
+ let lockDir = getLockDir();
371
+ const files = await fp.readdir(lockDir);
372
+ const deadPid = {}
373
+ for (let file of files) {
374
+ let lockKeyDir = lockDir + '/' + file;
375
+ let pid = await readLockPid(lockKeyDir);
376
+ if (!pid) {
377
+ continue;
378
+ }
379
+ if (deadPid[pid] || !isPidAlive(pid)) {
380
+ deadPid[pid] = true;
381
+ // 持有锁的进程已经没了,则删除锁文件
382
+ try {
383
+ await fp.rm(lockKeyDir, { recursive: true, force: true });
384
+ } catch (err) {
385
+ console.$error(`cleanLock ${lockKeyDir} failed`, err);
386
+ }
387
+ }
388
+ }
389
+ console.$debug("cleanFileLocks cost", `${Date.now() - now}ms`)
390
+ }
391
+
392
+ async function initRoom() {
393
+ // 每分钟清理缓存
394
+ cacheFn("jsir:cleanFnCache", cleanFnCache, 60_000)
345
395
  setting.roomTime = Date.now();
346
396
  setting.nodeMap = await fileJson(jsirNodesFile);
347
397
  let roomUpdateTouchTime = false;
@@ -353,6 +403,8 @@ async function initRoom() {
353
403
  }
354
404
  })
355
405
  if (roomUpdateTouchTime) {
406
+ // 每分钟清理lock 文件
407
+ cacheFn("jsir:cleanFileLocks", cleanFileLocks, 60_000)
356
408
  fileLock(updateRoomInfoLockKey, async () => {
357
409
  let pros = []
358
410
  pros.push(syncRooms())
@@ -417,17 +469,14 @@ async function cleanRoom(room) {
417
469
  }
418
470
 
419
471
  /*
420
- 最小为0
421
- 100 为1 毫秒
472
+ < 0.3 很空
473
+ 0.3 ~ 0.6 正常
474
+ 0.6 ~ 0.8 明显忙
422
475
  */
423
476
  async function getEventLoopDelay() {
424
- const start = process.hrtime.bigint();
425
- const delay = 1; // 用于测量的短暂延迟(单位:毫秒)
426
- const timeoutPromise = new Promise(resolve => setTimeout(resolve, delay));
427
- return await timeoutPromise.then(() => {
428
- const end = process.hrtime.bigint();
429
- return Number(Math.max(Number(end - start) / 1e4 - (delay * 1e2), 0).toFixed(0)); // 避免负值
430
- });
477
+ const cur = performance.eventLoopUtilization(elu0); // 计算这段时间的差分
478
+ elu0 = performance.eventLoopUtilization(); // 更新基准点(关键!)
479
+ return Number(cur.utilization.toFixed(1));
431
480
  }
432
481
 
433
482
  async function initRoomJsir(room) {
@@ -457,7 +506,6 @@ async function initRoomJsir(room) {
457
506
  services[key] = [...new Set(services[key])]
458
507
  }
459
508
  // 初始化jsir
460
- let lastJsir = room.jsirs[process.pid]
461
509
  setting.selfJsir = {
462
510
  pid: process.pid,
463
511
  space: setting.defaultSpace,
@@ -465,7 +513,7 @@ async function initRoomJsir(room) {
465
513
  obj[key] = getValTips()[index];
466
514
  return obj;
467
515
  }, {}),
468
- busy: await getEventLoopDelay() || (lastJsir ? lastJsir.busy:0),
516
+ busy: await getEventLoopDelay(),
469
517
  back: isRunningInBackground(),
470
518
  lastUpdateTime: setting.roomTime,
471
519
  port: setting.server ? setting.server.address()?.port:null,
@@ -589,21 +637,27 @@ async function reqNode(node, method, url, port, body) {
589
637
  }
590
638
 
591
639
  function busyPick(busyItems) {
592
- if (busyItems.length === 1) {
593
- return busyItems[0]
640
+ if (busyItems.length === 1) return busyItems[0];
641
+
642
+ const weights = busyItems.map(item => {
643
+ // ELU 本来就是 0~1,这里只是兜底
644
+ const busy = Math.min(Math.max(item.busy ?? 1, 0), 1);
645
+ return 1 - busy; // 越忙权重越小
646
+ });
647
+
648
+ const total = weights.reduce((a, b) => a + b, 0);
649
+ if (total <= 0) {
650
+ // 所有节点都接近 1(极忙)时,随机选一个
651
+ return busyItems[Math.floor(Math.random() * busyItems.length)];
594
652
  }
595
- // 反转权重:计算 1 / busy 值作为权重
596
- const weights = busyItems.map(item => 1 / (item.busy || 1));
597
- const totalWeight = weights.reduce((sum, weight) => sum + weight, 0);
598
- const random = Math.random() * totalWeight;
599
653
 
600
- let cumulativeWeight = 0;
654
+ let r = Math.random() * total;
601
655
  for (let i = 0; i < busyItems.length; i++) {
602
- cumulativeWeight += weights[i];
603
- if (random < cumulativeWeight) {
604
- return busyItems[i]; // 选中反转权重命中的对象
605
- }
656
+ r -= weights[i];
657
+ if (r <= 0) return busyItems[i];
606
658
  }
659
+
660
+ return busyItems[busyItems.length - 1]; // 兜底
607
661
  }
608
662
 
609
663
  function isMatchTarget(target, node, port) {
package/deps/setting.js CHANGED
@@ -52,5 +52,6 @@ module.exports = {
52
52
  newError: false,
53
53
  fnCache: {},
54
54
  syncQueues: {},
55
- tipsOnRm: {}
55
+ tipsOnRm: {},
56
+ locks: {}
56
57
  }
package/deps/util.js CHANGED
@@ -627,11 +627,12 @@ async function draftModify(fLine) {
627
627
  console.info("removed")
628
628
  }
629
629
  if (isEdit && results.length === 1) {
630
- let tempPath = getLibDataDir() + "/log/draft.temp";
630
+ let tempPath = getTempDir() + '/draft_' + uid();
631
631
  fs.writeFileSync(tempPath, results[0].split(/\n/).slice(1).join('\n'))
632
632
  await eia(getEditor(), [`"${tempPath}"`], true)
633
633
  let tempText = String(fs.readFileSync(tempPath))
634
- fs.writeFileSync(tempPath, '')
634
+ fs.unlinkSync(tempPath)
635
+
635
636
  let lineRange = lineRanges.get(results[0]);
636
637
  let before = allLines.filter((_, index) => {
637
638
  return index < lineRange.start;
@@ -705,21 +706,21 @@ function getVl(...obj) {
705
706
 
706
707
  async function cacheFn(key, fn, validMs = 0, awaitRefresh = true) {
707
708
  const cacheItem = getOr(setting.fnCache, key, {
708
- createdAt: 0,
709
+ validMsTime: 0,
709
710
  });
710
- if (Date.now() - cacheItem.createdAt > validMs) {
711
- // 临时设为已创建,防止并发请求多次调用 fn
712
- cacheItem.createdAt = Infinity;
711
+ if (Date.now() > cacheItem.validMsTime) {
712
+ cacheItem.validMsTime = Infinity;
713
713
  cacheItem.promise = (async () => {
714
714
  let val;
715
715
  try {
716
716
  val = await fn()
717
- } finally {
718
- cacheItem.createdAt = 0; // 恢复为无效状态
717
+ } catch (e) {
718
+ delete setting.fnCache[key]
719
+ throw e
719
720
  }
720
721
  cacheItem.val = val;
721
722
  cacheItem.valInit = true;
722
- cacheItem.createdAt = Date.now(); // 设置新的创建时间
723
+ cacheItem.validMsTime = Date.now() + validMs;
723
724
  })();
724
725
  }
725
726
  if (awaitRefresh || !cacheItem.valInit) {
@@ -770,13 +771,13 @@ function clearConsole(cleanType = 0) {
770
771
  if (cleanType === 0) {
771
772
  // \033[2J:清空屏幕。
772
773
  // \033[H:将光标定位到屏幕左上角。
773
- process.stdout.write('\033[2J\033[H'); // 展开新的屏幕,信息都保留
774
+ process.stdout.write('\x1B[2J\x1B[H'); // 展开新的屏幕,信息都保留
774
775
  } else if (cleanType === 1) {
775
776
  // \033[0f 清除从光标位置到屏幕末尾的内容,并把光标移动到屏幕的左上角。
776
777
  // process.stdout.write('\033[0f');
777
778
  console.clear(); // 清空当前屏幕
778
779
  } else if (cleanType > 1) {
779
- process.stdout.write('\x1B[3J\033[H'); // 全清,包括历史记录
780
+ process.stdout.write('\x1B[3J\x1B[H'); // 全清,包括历史记录
780
781
  console.clear();
781
782
  }
782
783
  }
@@ -1267,40 +1268,6 @@ function getLockKeyDir(key) {
1267
1268
  return lockDir + "/" + key;
1268
1269
  }
1269
1270
 
1270
- async function readLockPid(lockDir) {
1271
- try {
1272
- const files = await fp.readdir(lockDir);
1273
- const pidFile = files.find(name => name.startsWith('pid-'));
1274
- if (!pidFile) {
1275
- return null;
1276
- }
1277
- return pidFile.split("-")[1];
1278
- } catch (err) {
1279
- // 读取目录出错(可能不存在?)直接返回 null
1280
- return null;
1281
- }
1282
- }
1283
-
1284
- async function cleanFileLocks() {
1285
- let lockDir = getLockDir();
1286
- const files = await fp.readdir(lockDir);
1287
- for (let file of files) {
1288
- let lockKeyDir = lockDir + '/' + file;
1289
- let pid = await readLockPid(lockKeyDir);
1290
- if (!pid) {
1291
- continue;
1292
- }
1293
- if (!isPidAlive(pid)) {
1294
- // 持有锁的进程已经没了,则删除锁文件
1295
- try {
1296
- await fp.rm(lockKeyDir, { recursive: true, force: true });
1297
- } catch (err) {
1298
- console.$error(`cleanLock ${lockKeyDir} failed`, err);
1299
- }
1300
- }
1301
- }
1302
- }
1303
-
1304
1271
  async function _fileLock(key, fn) {
1305
1272
  `
1306
1273
  文件锁,返回true/false,
@@ -1310,41 +1277,28 @@ async function _fileLock(key, fn) {
1310
1277
  throw new Error('invalid arguments');
1311
1278
  }
1312
1279
 
1313
- let lockKeyDir = getLockKeyDir(key)
1314
- // 1. 尝试判断锁目录是否已存在
1315
- let lockExists = await fileExist(lockKeyDir);
1316
- if (lockExists) {
1317
- return false;
1318
- }
1319
-
1320
- // 2. 创建锁目录
1321
- try {
1322
- await fp.mkdir(lockKeyDir, { recursive: false });
1323
- } catch (err) {
1324
- // 如果 mkdir 依旧失败,说明在这段时间里又被别人抢先创建了
1325
- return false;
1326
- }
1280
+ let tempLockDir = getLockDir() + '/lock_' + uid()
1281
+ // 创建临时锁目录
1282
+ await fp.mkdir(tempLockDir, { recursive: false });
1327
1283
 
1328
1284
  const lockPidFile = `pid-${process.pid}`;
1329
- const lockPidFilePath = path.join(lockKeyDir, lockPidFile);
1285
+ const lockPidFilePath = path.join(tempLockDir, lockPidFile);
1286
+ await fp.writeFile(lockPidFilePath, '');
1287
+
1288
+ let lockKeyDir = getLockKeyDir(key)
1330
1289
  try {
1331
- await fp.writeFile(lockPidFilePath, '');
1290
+ await fp.rename(tempLockDir, lockKeyDir);
1332
1291
  } catch (err) {
1333
- // 无法写文件就释放锁并抛错
1334
- try {
1335
- await fp.rm(lockKeyDir, { recursive: true, force: true });
1336
- } catch (_) {}
1292
+ try { await fp.rm(tempLockDir, { recursive: true, force: true }); } catch (_) {}
1337
1293
  return false;
1338
1294
  }
1339
1295
 
1340
- // 4. 成功加锁后执行 fn
1296
+ // 成功加锁后执行 fn
1341
1297
  try {
1342
1298
  await fn();
1343
1299
  return true;
1344
1300
  } finally {
1345
- try {
1346
- await fp.rm(lockKeyDir, { recursive: true, force: true });
1347
- } catch (_) {}
1301
+ try { await fp.rm(lockKeyDir, { recursive: true, force: true }); } catch (_) {}
1348
1302
  }
1349
1303
  }
1350
1304
 
@@ -1388,21 +1342,20 @@ async function processLock(key, fn, wait = true) {
1388
1342
  }
1389
1343
  }
1390
1344
 
1391
- const $locks = {}
1392
1345
  async function _processLock(key, fn) {
1393
1346
  if (!key || typeof fn !== 'function') {
1394
1347
  throw new Error('invalid arguments');
1395
1348
  }
1396
1349
  key = key.trim();
1397
- if ($locks.hasOwnProperty(key)) {
1350
+ if (setting.locks.hasOwnProperty(key)) {
1398
1351
  return false;
1399
1352
  }
1400
- $locks[key] = true;
1353
+ setting.locks[key] = true;
1401
1354
  try {
1402
1355
  await fn();
1403
1356
  return true;
1404
1357
  } finally {
1405
- delete $locks[key];
1358
+ delete setting.locks[key];
1406
1359
  }
1407
1360
  }
1408
1361
 
@@ -1519,6 +1472,7 @@ async function cleanFile(path, maxChars = 9 * 1024 * 1024) {
1519
1472
  const bakFile = `${path}.bak`;
1520
1473
  // 备份日志
1521
1474
  try {
1475
+ await fp.rm(bakFile, { force: true });
1522
1476
  await fp.rename(path, bakFile);
1523
1477
  } catch (e) {
1524
1478
  console.$error(`Failed to rename ${path} -> ${bakFile}`, e);
@@ -1608,41 +1562,86 @@ function ei(cmd, args = [], shell = false) {
1608
1562
  spawnSync(cmd, args, {stdio:"inherit", shell});
1609
1563
  }
1610
1564
 
1611
- async function eia(cmd, args = [], shell = false) {
1565
+ async function eia(cmd, args = [], shell = false, input = null) {
1612
1566
  `
1613
- 当前进程不会卡住,输入输出由cmd进程持有
1567
+ 当前进程不会卡住(事件循环仍在),但会 await 等子进程退出;
1568
+ 输入输出默认由 cmd 进程持有;如果传 input,则把 input 写入 stdin。
1614
1569
  `
1615
1570
  if (isRunningInBackground()) {
1616
1571
  throw 'Unsupported Operation';
1617
1572
  }
1618
1573
  setting.enableNextLine = false;
1619
- let child = spawn(cmd, args, {stdio:"inherit", shell});
1574
+
1575
+ // 如果需要写入 input,则 stdin 必须是 pipe
1576
+ const stdio = input != null
1577
+ ? ['pipe', 'inherit', 'inherit']
1578
+ : 'inherit';
1579
+
1580
+ const child = spawn(cmd, args, { stdio, shell });
1581
+
1582
+ // 把内存内容喂给子进程
1583
+ if (input != null) {
1584
+ child.stdin.write(input);
1585
+ child.stdin.end();
1586
+ }
1587
+
1620
1588
  return new Promise((resolve, reject) => {
1621
- // 监听子进程的关闭事件
1622
1589
  child.on('close', (code) => {
1623
1590
  setting.enableNextLine = true;
1624
1591
  resolve(code);
1625
1592
  });
1626
- // 可选:监听子进程的错误事件
1627
1593
  child.on('error', (err) => {
1628
1594
  setting.enableNextLine = true;
1629
- reject(err)
1595
+ reject(err);
1630
1596
  });
1631
- })
1597
+ });
1632
1598
  }
1633
1599
 
1634
- async function getCbText(mbNum) {
1635
- return await e(`pbpaste`, mbNum, false)
1600
+ /**
1601
+ * 获取剪贴板文本(macOS)
1602
+ * @returns {Promise<string|null>}
1603
+ */
1604
+ async function getCbText() {
1605
+ return new Promise((resolve) => {
1606
+ try {
1607
+ const p = spawn("pbpaste");
1608
+ let out = "";
1609
+
1610
+ p.stdout.setEncoding("utf8");
1611
+ p.stdout.on("data", (d) => out += d);
1612
+
1613
+ p.on("error", () => resolve(null));
1614
+ p.on("close", (code) => {
1615
+ if (code === 0) resolve(out);
1616
+ else resolve(null);
1617
+ });
1618
+ } catch (e) {
1619
+ resolve(null);
1620
+ }
1621
+ });
1636
1622
  }
1637
1623
 
1624
+ /**
1625
+ * 设置剪贴板文本(macOS)
1626
+ * @param {string} str
1627
+ * @returns {Promise<boolean>}
1628
+ */
1638
1629
  async function setCbText(str) {
1639
- let tempDir = getLibDataDir() + '/temp'
1640
- mkdir(tempDir)
1630
+ if (typeof str !== "string") return false;
1641
1631
 
1642
- let copyFile = tempDir + '/pbcopy.' + String(Math.random()).split('.')[1]
1643
- fs.writeFileSync(copyFile, str)
1644
- await e(`pbcopy < ${copyFile}`)
1645
- fs.unlinkSync(copyFile)
1632
+ return new Promise((resolve) => {
1633
+ try {
1634
+ const p = spawn("pbcopy");
1635
+
1636
+ p.on("error", () => resolve(false));
1637
+ p.on("close", (code) => resolve(code === 0));
1638
+
1639
+ p.stdin.setDefaultEncoding("utf8");
1640
+ p.stdin.end(str);
1641
+ } catch (e) {
1642
+ resolve(false);
1643
+ }
1644
+ });
1646
1645
  }
1647
1646
 
1648
1647
  async function sleep(milliseconds, tIds = []) {
@@ -2368,7 +2367,6 @@ module.exports = {
2368
2367
  hasTips,
2369
2368
  tipKeys,
2370
2369
  isPidAlive,
2371
- cleanFileLocks,
2372
2370
  getMd5Key,
2373
2371
  isMd5Key,
2374
2372
  terminalTitle,
@@ -2380,5 +2378,6 @@ module.exports = {
2380
2378
  currentRoom,
2381
2379
  currentRooms,
2382
2380
  uid,
2383
- consoleStrs
2381
+ consoleStrs,
2382
+ getLockDir
2384
2383
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsir",
3
- "version": "3.1.1",
3
+ "version": "3.1.3",
4
4
  "description": "JavaScript Script Management Tool",
5
5
  "main": "index.js",
6
6
  "scripts": {