@xiaoyuanhao/yunzhi-cli 2.1.2 → 2.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.
@@ -1,6 +1,7 @@
1
1
  // Conversation authorization state is local to the command working directory.
2
2
  const fs = require('node:fs/promises');
3
3
  const path = require('node:path');
4
+ const net = require('node:net');
4
5
  const { createHash } = require('node:crypto');
5
6
  const { root, loadConfig } = require('./storage.cjs');
6
7
  const { gatewayURL } = require('./gateway.cjs');
@@ -18,9 +19,20 @@ async function withState(action, create = false) {
18
19
  const info = await fs.lstat(directory);
19
20
  if (!info.isDirectory() || info.isSymbolicLink()) throw stopped('授权上下文目录无效。');
20
21
  const file = path.join(directory, hash(process.cwd()) + '.json');
21
- let lock;
22
- try { lock = await fs.open(file + '.lock', 'wx', 0o600); }
23
- catch (error) { if (error.code === 'EEXIST') throw stopped('本工作目录的授权操作正在进行或上次被中断,请先检查该操作。'); throw error; }
22
+ // A loopback listener is only a process mutex; the OS releases it on exit.
23
+ // A port collision fails closed instead of choosing a different mutex.
24
+ const port = 49152 + parseInt(hash(path.resolve(file)).slice(0, 4), 16) % 16384;
25
+ const lock = net.createServer(socket => socket.destroy());
26
+ try {
27
+ await new Promise((resolve, reject) => {
28
+ lock.once('error', reject);
29
+ lock.listen({ host: '127.0.0.1', port, exclusive: true }, resolve);
30
+ });
31
+ } catch (error) {
32
+ throw stopped(error.code === 'EADDRINUSE'
33
+ ? '授权操作正在进行,或本机授权互斥端口被占用;本次未执行操作。'
34
+ : '无法取得本机授权互斥锁;本次未执行操作。');
35
+ }
24
36
  try {
25
37
  let state = null;
26
38
  try {
@@ -29,11 +41,12 @@ async function withState(action, create = false) {
29
41
  state = JSON.parse(await fs.readFile(file, 'utf8'));
30
42
  } catch (error) { if (error.code !== 'ENOENT') throw error; }
31
43
  const save = async value => {
32
- await fs.writeFile(file + '.tmp', JSON.stringify(value), { mode: 0o600, flag: 'wx' });
33
- await fs.rename(file + '.tmp', file);
44
+ const handle = await fs.open(file, 'w', 0o600);
45
+ try { await handle.writeFile(JSON.stringify(value)); await handle.sync(); }
46
+ finally { await handle.close(); }
34
47
  };
35
48
  return await action(state, save);
36
- } finally { await lock.close(); await fs.unlink(file + '.lock'); }
49
+ } finally { await new Promise((resolve, reject) => lock.close(error => error ? reject(error) : resolve())); }
37
50
  }
38
51
  async function binding() {
39
52
  const config = await loadConfig();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xiaoyuanhao/yunzhi-cli",
3
- "version": "2.1.2",
3
+ "version": "2.1.3",
4
4
  "description": "Yunzhi Campus CLI for lesson preparation and school library workflows",
5
5
  "bin": {
6
6
  "yunzhi-cli": "yunzhi-cli.cjs"