dsh-tg-bridge 1.3.4 → 1.3.5

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/bridge-kick.mjs CHANGED
@@ -8,7 +8,7 @@
8
8
  //
9
9
  // Exit codes: 0 = nothing to do or started, 2 = start failed.
10
10
 
11
- import { spawnSync } from 'node:child_process';
11
+ import { spawn, spawnSync } from 'node:child_process';
12
12
  import { appendFileSync, existsSync, readFileSync, openSync, closeSync, unlinkSync, writeFileSync } from 'node:fs';
13
13
  import { homedir } from 'node:os';
14
14
  import { join } from 'node:path';
@@ -21,25 +21,48 @@ const KICK_LOG = join(DIR, 'kick.log');
21
21
 
22
22
  // Shared with setup.mjs: a restart holds this lock, so a tick that lands in the
23
23
  // middle of one must not start a second poller.
24
+ //
25
+ // A live holder used to disable supervision forever: the old code started the
26
+ // bridge with spawnSync(), which blocks until the child EXITS — for a long-lived
27
+ // bridge the tick never returned, kept this lock for hours, and every later tick
28
+ // logged "restart in progress" and skipped (2026-10-24: supervisor dead from
29
+ // 18:57 while the bridge lived). Two fixes: spawn() + unref() below, and
30
+ // LOCK_MAX_AGE_MS here so even a genuinely wedged holder cannot wedge the guard.
24
31
  const LOCK = join(DIR, 'restart.lock');
32
+ const LOCK_MAX_AGE_MS = 180_000;
33
+
25
34
  function acquireLock(owner) {
26
35
  for (let attempt = 0; attempt < 2; attempt += 1) {
27
36
  try {
28
37
  const fd = openSync(LOCK, 'wx');
29
38
  writeFileSync(fd, JSON.stringify({ pid: process.pid, owner, ts: Date.now() }));
30
39
  closeSync(fd);
31
- const release = () => { try { unlinkSync(LOCK); } catch {} };
40
+ // Remove only our own lock: after a stale-lock takeover the file belongs
41
+ // to the new holder, and a late exit must not steal it.
42
+ const release = () => {
43
+ try {
44
+ if (Number(JSON.parse(readFileSync(LOCK, 'utf8')).pid) === process.pid) unlinkSync(LOCK);
45
+ } catch {}
46
+ };
32
47
  process.on('exit', release);
33
48
  return release;
34
49
  } catch (error) {
35
50
  if (error.code !== 'EEXIST') return null;
36
51
  let held = 0;
37
- try { held = Number(JSON.parse(readFileSync(LOCK, 'utf8')).pid) || 0; } catch {}
52
+ let heldTs = 0;
53
+ try {
54
+ const info = JSON.parse(readFileSync(LOCK, 'utf8'));
55
+ held = Number(info.pid) || 0;
56
+ heldTs = Number(info.ts) || 0;
57
+ } catch {}
58
+ const age = heldTs ? Date.now() - heldTs : Number.POSITIVE_INFINITY;
38
59
  let alive = false;
39
60
  if (held) {
40
61
  try { process.kill(held, 0); alive = true; } catch (e) { alive = e.code === 'EPERM'; }
41
62
  }
42
- if (alive) return null;
63
+ // Age wins over liveness: a holder that has not finished a restart in
64
+ // LOCK_MAX_AGE_MS is wedged, not busy.
65
+ if (alive && age < LOCK_MAX_AGE_MS) return null;
43
66
  try { unlinkSync(LOCK); } catch {}
44
67
  }
45
68
  }
@@ -93,25 +116,37 @@ function main() {
93
116
  const restartPending = existsSync(marker);
94
117
 
95
118
  const filePid = pidFromFile();
119
+ // Cheap path first: a live pid file is authoritative for a healthy bridge, so
120
+ // the minute tick costs one kill(0) instead of a PowerShell process scan.
121
+ if (filePid && alive(filePid)) {
122
+ if (restartPending) log(`marker present but pid ${filePid} alive`);
123
+ return 0;
124
+ }
125
+ // Only when the pid file is stale or missing: scan for a bridge that started
126
+ // without writing one.
96
127
  const live = bridgePids();
97
- if (live.length > 0 || (filePid && alive(filePid))) {
98
- if (restartPending && live.length === 0) log(`marker present but pid ${filePid} alive`);
128
+ if (live.length > 0) {
129
+ log(`pid file stale (${filePid || 'missing'}), bridge ${live.join(',')} alive — not starting`);
99
130
  return 0;
100
131
  }
101
132
 
102
133
  log(restartPending ? 'marker present and no bridge: starting' : 'no bridge: starting');
103
- const child = spawnSync(process.execPath, ['--dns-result-order=ipv4first', BRIDGE], {
104
- cwd: DIR,
105
- detached: true,
106
- stdio: 'ignore',
107
- windowsHide: true,
108
- });
109
- if (child.error) {
110
- log(`start failed: ${child.error.message}`);
134
+ // spawn(), NOT spawnSync(): spawnSync waits for the child to exit, and this
135
+ // child is meant to outlive the tick.
136
+ let child;
137
+ try {
138
+ child = spawn(process.execPath, ['--dns-result-order=ipv4first', BRIDGE], {
139
+ cwd: DIR,
140
+ detached: true,
141
+ stdio: 'ignore',
142
+ windowsHide: true,
143
+ });
144
+ } catch (error) {
145
+ log(`start failed: ${error.message}`);
111
146
  return 2;
112
147
  }
113
- try { readFileSync; } catch {}
114
- log('start issued');
148
+ child.unref();
149
+ log(`start issued pid=${child.pid ?? '?'}`);
115
150
  return 0;
116
151
  }
117
152
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-tg-bridge",
3
- "version": "1.3.4",
3
+ "version": "1.3.5",
4
4
  "description": "Telegram bridge for DeepSeek Harness: text, voice, photos and project-per-topic sessions without opening the desktop app",
5
5
  "type": "module",
6
6
  "main": "bridge.mjs",
package/setup.mjs CHANGED
@@ -286,11 +286,30 @@ async function main() {
286
286
  if (recentConflict) fail('a stale poller still owns getUpdates (409 in the last 90s) — kill it, then re-run setup');
287
287
  else ok('no 409 conflicts in the last 90s');
288
288
 
289
+ // 4. Scheduled task: point it at the GUARDED HIDDEN launcher (wscript + bridge-kick).
290
+ // Two reasons it must be the guarded supervisor and not a direct node.exe start:
291
+ // (a) a console action on an Interactive-logon task pops a window at every logon
292
+ // and repetition — wscript with window style 0 never shows one;
293
+ // (b) starting bridge.mjs directly here races the one-minute tick: both see "no
294
+ // bridge" and both start a poller (409 getUpdates Conflict). Going through
295
+ // bridge-kick.mjs means every start happens under restart.lock after a check.
296
+ const vbsPath = join(BRIDGE_DIR, 'bridge-kick-hidden.vbs');
297
+ writeFileSync(
298
+ vbsPath,
299
+ [
300
+ "' hidden launcher: DSH bridge supervisor tick (no console window)",
301
+ 'Set sh = CreateObject("WScript.Shell")',
302
+ `sh.CurrentDirectory = "${BRIDGE_DIR}"`,
303
+ `rc = sh.Run("""${NODE}"" ""${join(BRIDGE_DIR, 'bridge-kick.mjs')}""", 0, True)`,
304
+ 'WScript.Quit rc',
305
+ '',
306
+ ].join('\r\n'),
307
+ );
289
308
  const task = run('powershell', [
290
309
  '-NoProfile', '-NonInteractive', '-Command',
291
- `$a = New-ScheduledTaskAction -Execute '${NODE}' -Argument '"--dns-result-order=ipv4first" "${BRIDGE}"' -WorkingDirectory '${BRIDGE_DIR}'; Set-ScheduledTask -TaskName 'DSH-TG-Bridge' -Action $a | Out-Null; 'ok'`,
310
+ `$a = New-ScheduledTaskAction -Execute 'C:\\Windows\\System32\\wscript.exe' -Argument '//B //Nologo "${vbsPath}"'; Set-ScheduledTask -TaskName 'DSH-TG-Bridge' -Action $a | Out-Null; 'ok'`,
292
311
  ]);
293
- if ((task.stdout || '').includes('ok')) ok('scheduled task DSH-TG-Bridge now uses the same flags');
312
+ if ((task.stdout || '').includes('ok')) ok('scheduled task DSH-TG-Bridge now runs the guarded hidden supervisor');
294
313
  else warn(`scheduled task not updated: ${(task.stderr || '').trim().slice(0, 160)}`);
295
314
 
296
315
  // 5. Bot menu.