@danieltmn/openbridge 0.5.0 → 0.5.2
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/CHANGELOG.md +17 -0
- package/README.md +2 -1
- package/package.json +1 -1
- package/src/cli.js +96 -1
- package/src/store/jsonfile.js +24 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ Todos los cambios relevantes de OpenBridge. Formato basado en
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/) y
|
|
5
5
|
[Versionado Semantico](https://semver.org/lang/es/).
|
|
6
6
|
|
|
7
|
+
## [0.5.2] - 2026-09-14
|
|
8
|
+
|
|
9
|
+
### Corregido
|
|
10
|
+
|
|
11
|
+
- Diagnostico de puerto ocupado: `status` y `server` muestran que **PID** usa el
|
|
12
|
+
puerto (suele ser otra casa con otro `--dir`) y, si el arranque en segundo plano
|
|
13
|
+
muere, se muestran las ultimas lineas de `server.log`.
|
|
14
|
+
- `store`: reintentos con backoff en el `rename` atomico (EPERM/EACCES/EBUSY en
|
|
15
|
+
Windows) y limpieza del `.tmp` si no se puede.
|
|
16
|
+
|
|
17
|
+
## [0.5.1] - 2026-09-14
|
|
18
|
+
|
|
19
|
+
### Corregido
|
|
20
|
+
|
|
21
|
+
- `openbridge join` normaliza la URL del hub a `/api.php` (antes, pasar la base
|
|
22
|
+
o `/chat.php` apuntaba a `/` y el puente se quedaba sin trabajo).
|
|
23
|
+
|
|
7
24
|
## [0.5.0] - 2026-09-14
|
|
8
25
|
|
|
9
26
|
### Agregado
|
package/README.md
CHANGED
|
@@ -133,7 +133,8 @@ Ejemplo concreto:
|
|
|
133
133
|
3. En la **PC 2** (remota):
|
|
134
134
|
```bash
|
|
135
135
|
openbridge join https://tu-url-publica --token <bridgeToken> --id pc2 --name "PC 2"
|
|
136
|
-
#
|
|
136
|
+
# acepta la base, /chat.php o /api.php; se normaliza a /api.php
|
|
137
|
+
# con --no-start solo guarda la config
|
|
137
138
|
```
|
|
138
139
|
(equivale a `openbridge bridge --api … --token … --id … --name …`, pero
|
|
139
140
|
persiste la config para no repetir flags)
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -105,6 +105,70 @@ function pidAlive(pid) {
|
|
|
105
105
|
try { process.kill(pid, 0); return true; } catch (e) { return false; }
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
// PIDs escuchando en un puerto TCP (netstat en Windows; lsof en el resto). Sirve
|
|
109
|
+
// para explicar un EADDRINUSE: normalmente es OTRA casa (otro --dir) con el
|
|
110
|
+
// mismo puerto por defecto.
|
|
111
|
+
function portOwnerPids(port) {
|
|
112
|
+
try {
|
|
113
|
+
const win = process.platform === 'win32';
|
|
114
|
+
const r = spawnSync(win ? 'netstat' : 'lsof',
|
|
115
|
+
win ? ['-ano', '-p', 'tcp'] : ['-t', '-i', 'tcp:' + port, '-s', 'tcp:listen'],
|
|
116
|
+
{ encoding: 'utf8', windowsHide: true, timeout: 15000 });
|
|
117
|
+
if (r.error || !r.stdout) return [];
|
|
118
|
+
const pids = new Set();
|
|
119
|
+
if (!win) {
|
|
120
|
+
for (const n of String(r.stdout).split(/\s+/)) {
|
|
121
|
+
const p = parseInt(n, 10);
|
|
122
|
+
if (p > 0) pids.add(p);
|
|
123
|
+
}
|
|
124
|
+
return [...pids];
|
|
125
|
+
}
|
|
126
|
+
const re = new RegExp(':' + port + '\\s');
|
|
127
|
+
for (const line of String(r.stdout).split(/\r?\n/)) {
|
|
128
|
+
if (!re.test(line) || !/LISTENING/i.test(line)) continue;
|
|
129
|
+
const parts = line.trim().split(/\s+/);
|
|
130
|
+
const pid = parseInt(parts[parts.length - 1], 10);
|
|
131
|
+
if (pid > 0) pids.add(pid);
|
|
132
|
+
}
|
|
133
|
+
return [...pids];
|
|
134
|
+
} catch (e) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Nombre del proceso de un PID (best-effort) para el diagnostico del puerto.
|
|
140
|
+
function describePid(pid) {
|
|
141
|
+
try {
|
|
142
|
+
if (process.platform === 'win32') {
|
|
143
|
+
const r = spawnSync('tasklist', ['/FI', 'PID eq ' + pid, '/FO', 'CSV', '/NH'], { encoding: 'utf8', windowsHide: true, timeout: 8000 });
|
|
144
|
+
const m = String(r.stdout || '').match(/^"([^"]+)"/m);
|
|
145
|
+
return m ? m[1] : '';
|
|
146
|
+
}
|
|
147
|
+
const r = spawnSync('ps', ['-p', String(pid), '-o', 'comm='], { encoding: 'utf8', windowsHide: true, timeout: 8000 });
|
|
148
|
+
return String(r.stdout || '').trim();
|
|
149
|
+
} catch (e) {
|
|
150
|
+
return '';
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// "PID 123 (node.exe)" para el proceso que ocupa el puerto, o '' si no hay.
|
|
155
|
+
function portHint(port) {
|
|
156
|
+
const pids = portOwnerPids(port);
|
|
157
|
+
if (!pids.length) return '';
|
|
158
|
+
return pids.map((p) => 'PID ' + p + (describePid(p) ? ' (' + describePid(p) + ')' : '')).join(', ');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Ultimas `n` lineas no vacias de un archivo (para mostrar el error real del
|
|
162
|
+
// server cuando arranca en segundo plano y muere).
|
|
163
|
+
function tailLines(file, n) {
|
|
164
|
+
try {
|
|
165
|
+
const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/).filter((l) => l.trim() !== '');
|
|
166
|
+
return lines.slice(-n).join('\n');
|
|
167
|
+
} catch (e) {
|
|
168
|
+
return '';
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
108
172
|
function runtimePath() { return paths.p('runtime.json'); }
|
|
109
173
|
function readRuntime() {
|
|
110
174
|
try { return JSON.parse(fs.readFileSync(runtimePath(), 'utf8')); } catch (e) { return null; }
|
|
@@ -328,6 +392,12 @@ async function cmdServer(argv) {
|
|
|
328
392
|
await printStatus();
|
|
329
393
|
if (!rt) {
|
|
330
394
|
console.log('');
|
|
395
|
+
const tail = tailLines(paths.serverLogPath(), 8);
|
|
396
|
+
if (tail) {
|
|
397
|
+
console.log('Ultimas lineas de ' + paths.serverLogPath() + ':');
|
|
398
|
+
console.log(tail);
|
|
399
|
+
console.log('');
|
|
400
|
+
}
|
|
331
401
|
console.log('No pude confirmar el arranque. Revisa: openbridge logs');
|
|
332
402
|
}
|
|
333
403
|
return 0;
|
|
@@ -349,6 +419,12 @@ async function cmdServer(argv) {
|
|
|
349
419
|
} catch (e) {
|
|
350
420
|
if (e && e.code === 'EADDRINUSE') {
|
|
351
421
|
log.error('puerto ' + port + ' ocupado (¿ya corre OpenBridge?). Usa --port <otro> o `openbridge stop`.');
|
|
422
|
+
const hint = portHint(port);
|
|
423
|
+
if (hint) {
|
|
424
|
+
console.error(' lo esta usando: ' + hint);
|
|
425
|
+
console.error(' puede ser otra casa (otro --dir): proba `openbridge stop --dir <esa-casa>`'
|
|
426
|
+
+ (process.platform === 'win32' ? ' o `taskkill /PID ' + portOwnerPids(port)[0] + ' /T /F`.' : '.'));
|
|
427
|
+
}
|
|
352
428
|
return 1;
|
|
353
429
|
}
|
|
354
430
|
throw e;
|
|
@@ -443,6 +519,14 @@ async function printStatus() {
|
|
|
443
519
|
if (paths.exists()) {
|
|
444
520
|
console.log(' carpeta: ' + paths.baseDir());
|
|
445
521
|
console.log(' datos : ' + paths.home());
|
|
522
|
+
try {
|
|
523
|
+
const app = config.readApp();
|
|
524
|
+
const hint = portHint(app.port);
|
|
525
|
+
if (hint) {
|
|
526
|
+
console.log(' aviso : el puerto ' + app.port + ' ya lo usa ' + hint);
|
|
527
|
+
console.log(' suele ser otra casa (otro --dir); cerrala con `openbridge stop --dir <esa-casa>` o usa --port <otro>.');
|
|
528
|
+
}
|
|
529
|
+
} catch (e) { /* sin app.json no hay puerto que mirar */ }
|
|
446
530
|
}
|
|
447
531
|
return false;
|
|
448
532
|
}
|
|
@@ -624,17 +708,28 @@ async function cmdBridge(argv) {
|
|
|
624
708
|
// join: esta PC se suma como puente de un hub (otra PC con `openbridge server`).
|
|
625
709
|
// Guarda la URL/token/identidad en config.json y arranca el puente.
|
|
626
710
|
// ---------------------------------------------------------------------------
|
|
711
|
+
// Normaliza la URL del hub a su endpoint de API: acepta la base, /chat.php o
|
|
712
|
+
// /api.php y devuelve siempre algo terminado en /api.php (lo que espera el
|
|
713
|
+
// puente). Sin esto, un `join https://host` apuntaba a / y no al API.
|
|
714
|
+
function hubApiUrl(raw) {
|
|
715
|
+
let u = String(raw || '').trim().replace(/\/+$/, '');
|
|
716
|
+
u = u.replace(/\/chat\.php$/i, '');
|
|
717
|
+
if (!/\/api\.php$/i.test(u)) u += '/api.php';
|
|
718
|
+
return u;
|
|
719
|
+
}
|
|
720
|
+
|
|
627
721
|
async function cmdJoin(argv) {
|
|
628
722
|
const { flags, _ } = parseArgs(argv);
|
|
629
723
|
const url = String(_[0] || flags.api || '').trim();
|
|
630
724
|
if (!url || !/^https?:\/\//i.test(url)) {
|
|
631
725
|
console.error('Uso: openbridge join <url-del-hub> [--token <t>] [--id <pc>] [--name "<nombre>"] [--no-start]');
|
|
632
726
|
console.error('Ej.: openbridge join https://mi-pc.trycloudflare.com --token <t> --id pc2 --name "PC oficina"');
|
|
727
|
+
console.error('(acepta la base, /chat.php o /api.php; se normaliza a /api.php)');
|
|
633
728
|
return 1;
|
|
634
729
|
}
|
|
635
730
|
paths.ensureDirs();
|
|
636
731
|
const cfg = config.readBridge();
|
|
637
|
-
cfg.apiUrl = url
|
|
732
|
+
cfg.apiUrl = hubApiUrl(url);
|
|
638
733
|
if (typeof flags.token === 'string') cfg.apiToken = flags.token;
|
|
639
734
|
if (typeof flags.id === 'string') cfg.bridgeId = sanitizeId(flags.id);
|
|
640
735
|
else if (!cfg.bridgeId) cfg.bridgeId = sanitizeId(os.hostname());
|
package/src/store/jsonfile.js
CHANGED
|
@@ -12,6 +12,29 @@ const fs = require('node:fs/promises');
|
|
|
12
12
|
|
|
13
13
|
const locks = new Map();
|
|
14
14
|
|
|
15
|
+
function delay(ms) {
|
|
16
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// En Windows, rename() puede fallar con EPERM/EACCES si otro proceso (antivirus,
|
|
20
|
+
// indexador) tiene el archivo abierto un instante. Reintentamos con backoff y,
|
|
21
|
+
// si no hay forma, limpiamos el .tmp.
|
|
22
|
+
async function renameWithRetry(from, to, tries = 5) {
|
|
23
|
+
for (let i = 0; ; i++) {
|
|
24
|
+
try {
|
|
25
|
+
await fs.rename(from, to);
|
|
26
|
+
return;
|
|
27
|
+
} catch (e) {
|
|
28
|
+
const transient = e && (e.code === 'EPERM' || e.code === 'EACCES' || e.code === 'EBUSY');
|
|
29
|
+
if (!transient || i >= tries - 1) {
|
|
30
|
+
try { await fs.unlink(from); } catch (e2) { /* nada */ }
|
|
31
|
+
throw e;
|
|
32
|
+
}
|
|
33
|
+
await delay(20 * (i + 1));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
15
38
|
function withLock(key, fn) {
|
|
16
39
|
const prev = locks.get(key) || Promise.resolve();
|
|
17
40
|
const run = prev.then(fn, fn);
|
|
@@ -33,7 +56,7 @@ async function readJson(file, fallback) {
|
|
|
33
56
|
async function writeAtomic(file, data) {
|
|
34
57
|
const tmp = file + '.' + process.pid + '.' + Math.random().toString(36).slice(2) + '.tmp';
|
|
35
58
|
await fs.writeFile(tmp, JSON.stringify(data, null, 2));
|
|
36
|
-
await
|
|
59
|
+
await renameWithRetry(tmp, file);
|
|
37
60
|
}
|
|
38
61
|
|
|
39
62
|
/**
|