@getparallel/parx 0.5.0 → 0.7.0
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 +15 -0
- package/README.md +11 -5
- package/dist/actualizar.js +2 -1
- package/dist/agente.js +82 -2
- package/dist/api.js +17 -0
- package/dist/cli.js +351 -35
- package/dist/config.js +6 -0
- package/dist/estimacion.js +118 -0
- package/dist/firmas.js +55 -0
- package/dist/marca.js +56 -19
- package/dist/mision.js +4 -3
- package/dist/par.js +355 -0
- package/dist/remoto.js +295 -0
- package/dist/servicio.js +191 -0
- package/dist/tar.js +99 -0
- package/dist/textos.js +68 -7
- package/dist/tui.js +56 -10
- package/dist/ui.js +45 -2
- package/dist/versiones.js +341 -0
- package/dist/vista.js +133 -0
- package/package.json +1 -1
package/dist/remoto.js
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { abrirRemoto, cerrarRemoto, ErrorDeParallel, latidoRemoto, ordenesRemotas } from "./api.js";
|
|
5
|
+
import { sinColor } from "./marca.js";
|
|
6
|
+
export function seContestaDesdeFuera(clase) {
|
|
7
|
+
return clase !== 'credencial' && clase !== 'bypass';
|
|
8
|
+
}
|
|
9
|
+
const SECRETOS = [
|
|
10
|
+
/\b(sk|pk|rk)[-_](live|test|proj|ant)?[-_]?[A-Za-z0-9_-]{16,}/g,
|
|
11
|
+
/\bgh[pousr]_[A-Za-z0-9]{20,}/g,
|
|
12
|
+
/\bxox[abprs]-[A-Za-z0-9-]{10,}/g,
|
|
13
|
+
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
14
|
+
/\bAIza[0-9A-Za-z_-]{30,}/g,
|
|
15
|
+
/\bpar_(term|key)_[A-Za-z0-9_-]{8,}/g,
|
|
16
|
+
/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
|
|
17
|
+
];
|
|
18
|
+
const CON_NOMBRE = [/\b(Bearer\s+)[A-Za-z0-9._~+/-]{16,}=*/gi, /\b((?:password|passwd|secret|token|api[_-]?key)\s*[:=]\s*['"]?)[^\s'"]{6,}/gi];
|
|
19
|
+
export function taparSecretos(s) {
|
|
20
|
+
let r = s;
|
|
21
|
+
for (const re of CON_NOMBRE)
|
|
22
|
+
r = r.replace(re, (_m, nombre) => `${nombre}[hidden]`);
|
|
23
|
+
for (const re of SECRETOS)
|
|
24
|
+
r = r.replace(re, '[hidden]');
|
|
25
|
+
return r;
|
|
26
|
+
}
|
|
27
|
+
const MAX_TEXTO = 8000;
|
|
28
|
+
export function paraElMovil(v, max = MAX_TEXTO) {
|
|
29
|
+
const s = taparSecretos(sinColor(typeof v === 'string' ? v : v == null ? '' : String(v)));
|
|
30
|
+
return s.length > max ? `${s.slice(0, max - 1)}…` : s;
|
|
31
|
+
}
|
|
32
|
+
function limpiar(datos) {
|
|
33
|
+
const out = {};
|
|
34
|
+
for (const [k, v] of Object.entries(datos)) {
|
|
35
|
+
if (typeof v === 'string')
|
|
36
|
+
out[k] = paraElMovil(v);
|
|
37
|
+
else if (Array.isArray(v))
|
|
38
|
+
out[k] = v.slice(0, 60).map((x) => (typeof x === 'string' ? paraElMovil(x, 400) : x && typeof x === 'object' ? limpiar(x) : x));
|
|
39
|
+
else if (v && typeof v === 'object')
|
|
40
|
+
out[k] = limpiar(v);
|
|
41
|
+
else
|
|
42
|
+
out[k] = v;
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
const API = { abrir: abrirRemoto, latido: latidoRemoto, ordenes: ordenesRemotas, cerrar: cerrarRemoto };
|
|
47
|
+
let contadorDePreguntas = 0;
|
|
48
|
+
export class Remoto {
|
|
49
|
+
id = null;
|
|
50
|
+
url = null;
|
|
51
|
+
o;
|
|
52
|
+
api;
|
|
53
|
+
n = 0;
|
|
54
|
+
cola = [];
|
|
55
|
+
estado = 'libre';
|
|
56
|
+
tarea = null;
|
|
57
|
+
acciones = 0;
|
|
58
|
+
modo = '';
|
|
59
|
+
modelo = '';
|
|
60
|
+
activo = false;
|
|
61
|
+
enviando = false;
|
|
62
|
+
otraVez = false;
|
|
63
|
+
relojEnvio = null;
|
|
64
|
+
relojLatido = null;
|
|
65
|
+
escucha = null;
|
|
66
|
+
ultimoEnvio = 0;
|
|
67
|
+
preguntas = new Map();
|
|
68
|
+
constructor(o) {
|
|
69
|
+
this.o = o;
|
|
70
|
+
this.api = { ...API, ...o.api };
|
|
71
|
+
}
|
|
72
|
+
get encendido() {
|
|
73
|
+
return this.activo;
|
|
74
|
+
}
|
|
75
|
+
async abrir(datos) {
|
|
76
|
+
const r = await this.api.abrir(this.o.servidor, this.o.token, datos);
|
|
77
|
+
this.id = r.id;
|
|
78
|
+
this.url = r.url;
|
|
79
|
+
this.modo = datos.modo;
|
|
80
|
+
this.modelo = datos.modelo;
|
|
81
|
+
this.activo = true;
|
|
82
|
+
this.relojLatido = setInterval(() => void this.enviar(), this.o.latidoMs ?? 15_000);
|
|
83
|
+
this.relojLatido.unref?.();
|
|
84
|
+
void this.enviar();
|
|
85
|
+
void this.escuchar();
|
|
86
|
+
return r;
|
|
87
|
+
}
|
|
88
|
+
async cerrar() {
|
|
89
|
+
if (!this.activo)
|
|
90
|
+
return;
|
|
91
|
+
this.apagar();
|
|
92
|
+
if (this.id) {
|
|
93
|
+
try {
|
|
94
|
+
await this.api.cerrar(this.o.servidor, this.o.token, this.id);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
apagar() {
|
|
101
|
+
this.activo = false;
|
|
102
|
+
if (this.carpeta && !this.o.carpetaDeFotos)
|
|
103
|
+
rmSync(this.carpeta, { recursive: true, force: true });
|
|
104
|
+
if (this.relojEnvio)
|
|
105
|
+
clearTimeout(this.relojEnvio);
|
|
106
|
+
if (this.relojLatido)
|
|
107
|
+
clearInterval(this.relojLatido);
|
|
108
|
+
this.relojEnvio = this.relojLatido = null;
|
|
109
|
+
this.escucha?.abort();
|
|
110
|
+
this.escucha = null;
|
|
111
|
+
this.preguntas.clear();
|
|
112
|
+
}
|
|
113
|
+
evento(tipo, datos) {
|
|
114
|
+
if (!this.activo)
|
|
115
|
+
return;
|
|
116
|
+
this.cola.push({ n: this.n++, tipo, datos: limpiar(datos) });
|
|
117
|
+
if (!this.relojEnvio) {
|
|
118
|
+
this.relojEnvio = setTimeout(() => {
|
|
119
|
+
this.relojEnvio = null;
|
|
120
|
+
void this.enviar();
|
|
121
|
+
}, this.o.cadaMs ?? 400);
|
|
122
|
+
this.relojEnvio.unref?.();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
fijarEstado(estado, tarea) {
|
|
126
|
+
if (!this.activo)
|
|
127
|
+
return;
|
|
128
|
+
const cambia = estado !== this.estado || (tarea !== undefined && tarea !== this.tarea);
|
|
129
|
+
this.estado = estado;
|
|
130
|
+
if (tarea !== undefined)
|
|
131
|
+
this.tarea = tarea ? paraElMovil(tarea, 200) : null;
|
|
132
|
+
if (cambia)
|
|
133
|
+
void this.enviar();
|
|
134
|
+
}
|
|
135
|
+
fijarModo(modo, modelo) {
|
|
136
|
+
this.modo = modo;
|
|
137
|
+
this.modelo = modelo;
|
|
138
|
+
}
|
|
139
|
+
progreso(acciones) {
|
|
140
|
+
this.acciones = acciones;
|
|
141
|
+
}
|
|
142
|
+
preguntaRemota(texto, clase, opciones) {
|
|
143
|
+
const id = `p${Date.now().toString(36)}${(contadorDePreguntas++).toString(36)}`;
|
|
144
|
+
const fuera = seContestaDesdeFuera(clase);
|
|
145
|
+
let deFuera = null;
|
|
146
|
+
const valores = opciones.map((x) => x.valor);
|
|
147
|
+
if (this.activo && fuera)
|
|
148
|
+
deFuera = new Promise((resolver) => this.preguntas.set(id, { resolver, valores }));
|
|
149
|
+
const anterior = this.estado;
|
|
150
|
+
this.evento('pregunta', { id, texto, clase, opciones: opciones.map((x) => ({ etiqueta: x.etiqueta, valor: x.valor })), remota: fuera });
|
|
151
|
+
this.fijarEstado('pregunta');
|
|
152
|
+
return {
|
|
153
|
+
id,
|
|
154
|
+
deFuera,
|
|
155
|
+
contestada: (valor, desde) => {
|
|
156
|
+
this.preguntas.delete(id);
|
|
157
|
+
this.evento('respuesta', { id, valor, desde });
|
|
158
|
+
this.fijarEstado(anterior === 'pregunta' ? 'trabajando' : anterior);
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
async enviar() {
|
|
163
|
+
if (!this.activo || !this.id)
|
|
164
|
+
return;
|
|
165
|
+
if (this.enviando) {
|
|
166
|
+
this.otraVez = true;
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
this.enviando = true;
|
|
170
|
+
try {
|
|
171
|
+
const tanda = this.cola.slice(0, 200);
|
|
172
|
+
const r = await this.api.latido(this.o.servidor, this.o.token, this.id, {
|
|
173
|
+
estado: this.estado,
|
|
174
|
+
tarea: this.tarea,
|
|
175
|
+
acciones: this.acciones,
|
|
176
|
+
modo: this.modo,
|
|
177
|
+
modelo: this.modelo,
|
|
178
|
+
eventos: tanda,
|
|
179
|
+
});
|
|
180
|
+
this.cola.splice(0, tanda.length);
|
|
181
|
+
this.ultimoEnvio = Date.now();
|
|
182
|
+
if (r.cerrado) {
|
|
183
|
+
this.apagar();
|
|
184
|
+
this.o.alApagarse('web');
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
catch (e) {
|
|
189
|
+
if (this.cola.length > 2000)
|
|
190
|
+
this.cola.splice(0, this.cola.length - 2000);
|
|
191
|
+
if (e instanceof ErrorDeParallel && (e.codigo === 'sesion' || e.codigo === 'sin_acceso')) {
|
|
192
|
+
this.apagar();
|
|
193
|
+
this.o.alApagarse(e.codigo);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
this.enviando = false;
|
|
199
|
+
}
|
|
200
|
+
if (this.otraVez || (this.cola.length && !this.relojEnvio)) {
|
|
201
|
+
this.otraVez = false;
|
|
202
|
+
void this.enviar();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async escuchar() {
|
|
206
|
+
let pausa = 1000;
|
|
207
|
+
while (this.activo && this.id) {
|
|
208
|
+
this.escucha = new AbortController();
|
|
209
|
+
try {
|
|
210
|
+
const r = await this.api.ordenes(this.o.servidor, this.o.token, this.id, this.o.esperaMs ?? 25_000, this.escucha.signal);
|
|
211
|
+
pausa = 1000;
|
|
212
|
+
if (r.cerrado) {
|
|
213
|
+
this.apagar();
|
|
214
|
+
this.o.alApagarse('web');
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
for (const orden of r.ordenes)
|
|
218
|
+
this.cumplir(orden);
|
|
219
|
+
}
|
|
220
|
+
catch (e) {
|
|
221
|
+
if (!this.activo)
|
|
222
|
+
return;
|
|
223
|
+
if (e instanceof ErrorDeParallel && (e.codigo === 'sesion' || e.codigo === 'sin_acceso')) {
|
|
224
|
+
this.apagar();
|
|
225
|
+
this.o.alApagarse(e.codigo);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
await new Promise((r) => setTimeout(r, pausa).unref?.());
|
|
229
|
+
pausa = Math.min(30_000, pausa * 2);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
cumplir(orden) {
|
|
234
|
+
if (!this.activo)
|
|
235
|
+
return;
|
|
236
|
+
if (orden.tipo === 'mensaje') {
|
|
237
|
+
const texto = typeof orden.datos.texto === 'string' ? orden.datos.texto.trim().slice(0, MAX_TEXTO) : '';
|
|
238
|
+
const rutas = this.guardarFotos(orden);
|
|
239
|
+
const conFotos = [texto || (rutas.length ? 'Look at this photo.' : ''), ...rutas.map((r) => `"${r}"`)].filter(Boolean).join(' ');
|
|
240
|
+
if (conFotos)
|
|
241
|
+
this.o.alMensaje(conFotos);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (orden.tipo === 'parar') {
|
|
245
|
+
for (const [id, p] of this.preguntas) {
|
|
246
|
+
this.preguntas.delete(id);
|
|
247
|
+
p.resolver('no');
|
|
248
|
+
}
|
|
249
|
+
this.o.alParar();
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (orden.tipo === 'abrir') {
|
|
253
|
+
const carpeta = typeof orden.datos.carpeta === 'string' ? orden.datos.carpeta : '';
|
|
254
|
+
const texto = typeof orden.datos.texto === 'string' ? orden.datos.texto.trim().slice(0, MAX_TEXTO) : '';
|
|
255
|
+
if (carpeta && texto)
|
|
256
|
+
this.o.alAbrir?.({ orden: orden.id, carpeta, texto });
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (orden.tipo === 'respuesta') {
|
|
260
|
+
const id = String(orden.datos.pregunta ?? '');
|
|
261
|
+
const valor = orden.datos.valor;
|
|
262
|
+
const p = this.preguntas.get(id);
|
|
263
|
+
if (!p || (valor !== 'si' && valor !== 'siempre' && valor !== 'no') || !p.valores.includes(valor))
|
|
264
|
+
return;
|
|
265
|
+
this.preguntas.delete(id);
|
|
266
|
+
p.resolver(valor);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
carpeta = null;
|
|
270
|
+
guardarFotos(orden) {
|
|
271
|
+
const fotos = Array.isArray(orden.datos.fotos) ? orden.datos.fotos.slice(0, 3) : [];
|
|
272
|
+
const rutas = [];
|
|
273
|
+
fotos.forEach((f, i) => {
|
|
274
|
+
const ext = f.mime === 'image/png' ? 'png' : f.mime === 'image/webp' ? 'webp' : f.mime === 'image/jpeg' ? 'jpg' : null;
|
|
275
|
+
if (!ext || typeof f.datos !== 'string' || !/^[A-Za-z0-9+/]+={0,2}$/.test(f.datos))
|
|
276
|
+
return;
|
|
277
|
+
const buf = Buffer.from(f.datos, 'base64');
|
|
278
|
+
if (buf.length < 16 || buf.length > 2_500_000)
|
|
279
|
+
return;
|
|
280
|
+
try {
|
|
281
|
+
this.carpeta ??= this.o.carpetaDeFotos ?? join(tmpdir(), `parx-remoto-${process.pid}`);
|
|
282
|
+
mkdirSync(this.carpeta, { recursive: true, mode: 0o700 });
|
|
283
|
+
const ruta = join(this.carpeta, `photo-${orden.id.replace(/[^a-z0-9-]/gi, '').slice(0, 12)}-${i + 1}.${ext}`);
|
|
284
|
+
writeFileSync(ruta, buf, { mode: 0o600 });
|
|
285
|
+
rutas.push(ruta);
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
return rutas;
|
|
291
|
+
}
|
|
292
|
+
get msDesdeElUltimoEnvio() {
|
|
293
|
+
return this.ultimoEnvio ? Date.now() - this.ultimoEnvio : Number.POSITIVE_INFINITY;
|
|
294
|
+
}
|
|
295
|
+
}
|
package/dist/servicio.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { spawn, execFileSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { existsSync, mkdirSync, openSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { homedir, hostname } from 'node:os';
|
|
5
|
+
import { basename, join, resolve } from 'node:path';
|
|
6
|
+
import { equipo, ErrorDeParallel } from "./api.js";
|
|
7
|
+
import { guardarAjustes, leerAjustes } from "./config.js";
|
|
8
|
+
import { Remoto } from "./remoto.js";
|
|
9
|
+
export const ETIQUETA = 'net.getparallel.parx';
|
|
10
|
+
export const MAX_SESIONES = 3;
|
|
11
|
+
export function idDeCarpeta(ruta) {
|
|
12
|
+
return createHash('sha256').update(ruta).digest('hex').slice(0, 16);
|
|
13
|
+
}
|
|
14
|
+
export function carpetasDelServicio(rutas = leerAjustes().carpetasRemotas ?? []) {
|
|
15
|
+
return rutas
|
|
16
|
+
.filter((r) => {
|
|
17
|
+
try {
|
|
18
|
+
return existsSync(r) && statSync(r).isDirectory();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
.map((ruta) => ({ id: idDeCarpeta(ruta), nombre: basename(ruta) || ruta, ruta }));
|
|
25
|
+
}
|
|
26
|
+
export function anadirCarpeta(ruta) {
|
|
27
|
+
const abs = resolve(ruta);
|
|
28
|
+
if (!existsSync(abs) || !statSync(abs).isDirectory())
|
|
29
|
+
return null;
|
|
30
|
+
const real = realpathSync(abs);
|
|
31
|
+
if (real === homedir() || real === '/' || real === realpathSync(homedir()))
|
|
32
|
+
return null;
|
|
33
|
+
const lista = leerAjustes().carpetasRemotas ?? [];
|
|
34
|
+
if (!lista.includes(real))
|
|
35
|
+
guardarAjustes({ carpetasRemotas: [...lista, real].slice(-20) });
|
|
36
|
+
return real;
|
|
37
|
+
}
|
|
38
|
+
export function quitarCarpeta(ruta) {
|
|
39
|
+
const lista = leerAjustes().carpetasRemotas ?? [];
|
|
40
|
+
let real = resolve(ruta);
|
|
41
|
+
try {
|
|
42
|
+
real = realpathSync(real);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
}
|
|
46
|
+
const queda = lista.filter((x) => x !== real && basename(x) !== ruta);
|
|
47
|
+
guardarAjustes({ carpetasRemotas: queda });
|
|
48
|
+
return queda.length !== lista.length;
|
|
49
|
+
}
|
|
50
|
+
export const rutaDelPlist = () => join(homedir(), 'Library', 'LaunchAgents', `${ETIQUETA}.plist`);
|
|
51
|
+
export const rutaDelRegistro = () => join(homedir(), 'Library', 'Logs', 'parx-service.log');
|
|
52
|
+
const xml = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
53
|
+
export function plistDelServicio(o) {
|
|
54
|
+
const args = [o.node, ...o.argumentos, '--serve'].map((a) => ` <string>${xml(a)}</string>`).join('\n');
|
|
55
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
56
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
57
|
+
<plist version="1.0">
|
|
58
|
+
<dict>
|
|
59
|
+
<key>Label</key>
|
|
60
|
+
<string>${ETIQUETA}</string>
|
|
61
|
+
<key>ProgramArguments</key>
|
|
62
|
+
<array>
|
|
63
|
+
${args}
|
|
64
|
+
</array>
|
|
65
|
+
<key>RunAtLoad</key>
|
|
66
|
+
<true/>
|
|
67
|
+
<key>KeepAlive</key>
|
|
68
|
+
<dict>
|
|
69
|
+
<key>SuccessfulExit</key>
|
|
70
|
+
<false/>
|
|
71
|
+
</dict>
|
|
72
|
+
<key>ThrottleInterval</key>
|
|
73
|
+
<integer>30</integer>
|
|
74
|
+
<key>EnvironmentVariables</key>
|
|
75
|
+
<dict>
|
|
76
|
+
<key>PATH</key>
|
|
77
|
+
<string>${xml(o.path)}</string>
|
|
78
|
+
</dict>
|
|
79
|
+
<key>StandardOutPath</key>
|
|
80
|
+
<string>${xml(o.registro)}</string>
|
|
81
|
+
<key>StandardErrorPath</key>
|
|
82
|
+
<string>${xml(o.registro)}</string>
|
|
83
|
+
</dict>
|
|
84
|
+
</plist>
|
|
85
|
+
`;
|
|
86
|
+
}
|
|
87
|
+
const dominio = () => `gui/${process.getuid?.() ?? 501}`;
|
|
88
|
+
export function servicioInstalado() {
|
|
89
|
+
return existsSync(rutaDelPlist());
|
|
90
|
+
}
|
|
91
|
+
export function instalarServicio() {
|
|
92
|
+
if (process.platform !== 'darwin')
|
|
93
|
+
throw new Error('solo_mac');
|
|
94
|
+
const plist = rutaDelPlist();
|
|
95
|
+
mkdirSync(join(homedir(), 'Library', 'LaunchAgents'), { recursive: true });
|
|
96
|
+
mkdirSync(join(homedir(), 'Library', 'Logs'), { recursive: true });
|
|
97
|
+
writeFileSync(plist, plistDelServicio({ node: process.execPath, argumentos: [...process.execArgv, process.argv[1]], path: process.env.PATH ?? '/usr/bin:/bin', registro: rutaDelRegistro() }), { mode: 0o644 });
|
|
98
|
+
try {
|
|
99
|
+
execFileSync('launchctl', ['bootout', `${dominio()}/${ETIQUETA}`], { stdio: 'ignore' });
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
}
|
|
103
|
+
execFileSync('launchctl', ['bootstrap', dominio(), plist], { stdio: 'ignore' });
|
|
104
|
+
}
|
|
105
|
+
export function reiniciarServicio() {
|
|
106
|
+
if (!servicioInstalado())
|
|
107
|
+
return;
|
|
108
|
+
try {
|
|
109
|
+
execFileSync('launchctl', ['kickstart', '-k', `${dominio()}/${ETIQUETA}`], { stdio: 'ignore' });
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
export function quitarServicio() {
|
|
115
|
+
try {
|
|
116
|
+
execFileSync('launchctl', ['bootout', `${dominio()}/${ETIQUETA}`], { stdio: 'ignore' });
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
}
|
|
120
|
+
rmSync(rutaDelPlist(), { force: true });
|
|
121
|
+
}
|
|
122
|
+
export function servicioEnMarcha() {
|
|
123
|
+
try {
|
|
124
|
+
execFileSync('launchctl', ['print', `${dominio()}/${ETIQUETA}`], { stdio: 'ignore' });
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const log = (...a) => console.log(new Date().toISOString(), ...a);
|
|
132
|
+
const dormir = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
133
|
+
export function lanzarSesion(o) {
|
|
134
|
+
const salida = openSync(rutaDelRegistro(), 'a');
|
|
135
|
+
return spawn(process.execPath, [...process.execArgv, process.argv[1], '--remote-child', '--origen', o.orden, '--dir', o.ruta, '--', o.texto], {
|
|
136
|
+
cwd: o.ruta,
|
|
137
|
+
stdio: ['ignore', salida, salida],
|
|
138
|
+
env: { ...process.env, PARX_NO_UPDATE_CHECK: '1' },
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
export async function servir(cred) {
|
|
142
|
+
const carpetas = carpetasDelServicio();
|
|
143
|
+
if (!carpetas.length) {
|
|
144
|
+
log('no folders: run `parx service add` in a project folder');
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
let vivas = 0;
|
|
148
|
+
let fin = null;
|
|
149
|
+
const terminado = new Promise((r) => (fin = r));
|
|
150
|
+
const remoto = new Remoto({
|
|
151
|
+
servidor: cred.servidor,
|
|
152
|
+
token: cred.token,
|
|
153
|
+
alMensaje: () => { },
|
|
154
|
+
alParar: () => { },
|
|
155
|
+
alAbrir: ({ orden, carpeta, texto }) => {
|
|
156
|
+
const c = carpetasDelServicio().find((x) => x.id === carpeta);
|
|
157
|
+
if (!c)
|
|
158
|
+
return log('open: unknown folder', carpeta);
|
|
159
|
+
if (vivas >= MAX_SESIONES)
|
|
160
|
+
return log('open: too many sessions');
|
|
161
|
+
vivas++;
|
|
162
|
+
log('open', c.nombre);
|
|
163
|
+
const hijo = lanzarSesion({ ruta: c.ruta, orden, texto });
|
|
164
|
+
hijo.on('exit', () => vivas--);
|
|
165
|
+
},
|
|
166
|
+
alApagarse: (motivo) => {
|
|
167
|
+
log('stopped:', motivo);
|
|
168
|
+
fin?.(motivo === 'web' ? 0 : 1);
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
const e = equipo();
|
|
172
|
+
for (let pausa = 5000;; pausa = Math.min(300_000, pausa * 2)) {
|
|
173
|
+
try {
|
|
174
|
+
await remoto.abrir({ tipo: 'servicio', carpetas: carpetas.map(({ id, nombre }) => ({ id, nombre })), equipo: e.equipo || hostname(), version: e.version, carpeta: '', modelo: '', modo: '' });
|
|
175
|
+
log('listening:', carpetas.map((c) => c.nombre).join(', '));
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
if (err instanceof ErrorDeParallel && (err.codigo === 'sesion' || err.codigo === 'sin_acceso')) {
|
|
180
|
+
log('cannot start:', err.codigo);
|
|
181
|
+
return 1;
|
|
182
|
+
}
|
|
183
|
+
log('no connection, retrying');
|
|
184
|
+
await dormir(pausa);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const salir = () => void remoto.cerrar().finally(() => fin?.(0));
|
|
188
|
+
process.on('SIGTERM', salir);
|
|
189
|
+
process.on('SIGINT', salir);
|
|
190
|
+
return terminado;
|
|
191
|
+
}
|
package/dist/tar.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { gunzipSync } from 'node:zlib';
|
|
2
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, normalize, sep } from 'node:path';
|
|
4
|
+
const BLOQUE = 512;
|
|
5
|
+
function campo(b, desde, largo) {
|
|
6
|
+
const fin = b.indexOf(0, desde);
|
|
7
|
+
return b.subarray(desde, fin === -1 || fin > desde + largo ? desde + largo : fin).toString('utf8');
|
|
8
|
+
}
|
|
9
|
+
function numero(b, desde, largo) {
|
|
10
|
+
const s = campo(b, desde, largo).trim();
|
|
11
|
+
if (!s)
|
|
12
|
+
return 0;
|
|
13
|
+
if (b[desde] & 0x80) {
|
|
14
|
+
let n = 0;
|
|
15
|
+
for (let i = 1; i < largo; i++)
|
|
16
|
+
n = n * 256 + b[desde + i];
|
|
17
|
+
return n;
|
|
18
|
+
}
|
|
19
|
+
const n = parseInt(s, 8);
|
|
20
|
+
return Number.isFinite(n) ? n : 0;
|
|
21
|
+
}
|
|
22
|
+
export function leerTar(tar, limites = {}) {
|
|
23
|
+
const maxFicheros = limites.maxFicheros ?? 400;
|
|
24
|
+
const maxBytes = limites.maxBytes ?? 20 * 1024 * 1024;
|
|
25
|
+
const prefijo = limites.prefijo ?? 'package/';
|
|
26
|
+
const salida = [];
|
|
27
|
+
let bytes = 0;
|
|
28
|
+
let pos = 0;
|
|
29
|
+
let nombreLargo = null;
|
|
30
|
+
let rutaPax = null;
|
|
31
|
+
while (pos + BLOQUE <= tar.length) {
|
|
32
|
+
const cab = tar.subarray(pos, pos + BLOQUE);
|
|
33
|
+
if (cab.every((x) => x === 0))
|
|
34
|
+
break;
|
|
35
|
+
const tipo = String.fromCharCode(cab[156]);
|
|
36
|
+
const tamano = numero(cab, 124, 12);
|
|
37
|
+
const datos = tar.subarray(pos + BLOQUE, pos + BLOQUE + tamano);
|
|
38
|
+
pos += BLOQUE + Math.ceil(tamano / BLOQUE) * BLOQUE;
|
|
39
|
+
if (tipo === 'L') {
|
|
40
|
+
nombreLargo = datos.toString('utf8').replace(/\0+$/, '');
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (tipo === 'x') {
|
|
44
|
+
for (const linea of datos.toString('utf8').split('\n')) {
|
|
45
|
+
const m = /^\d+ path=(.*)$/.exec(linea);
|
|
46
|
+
if (m)
|
|
47
|
+
rutaPax = m[1];
|
|
48
|
+
}
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (tipo === 'g')
|
|
52
|
+
continue;
|
|
53
|
+
let nombre = rutaPax ?? nombreLargo ?? campo(cab, 0, 100);
|
|
54
|
+
if (!rutaPax && !nombreLargo && campo(cab, 257, 6) === 'ustar') {
|
|
55
|
+
const pre = campo(cab, 345, 155);
|
|
56
|
+
if (pre)
|
|
57
|
+
nombre = `${pre}/${nombre}`;
|
|
58
|
+
}
|
|
59
|
+
rutaPax = null;
|
|
60
|
+
nombreLargo = null;
|
|
61
|
+
if (tipo === '5' || nombre.endsWith('/'))
|
|
62
|
+
continue;
|
|
63
|
+
if (tipo !== '0' && tipo !== '\0' && tipo !== '7')
|
|
64
|
+
throw new Error(`unsupported entry type "${tipo}" for ${nombre}`);
|
|
65
|
+
if (!nombre.startsWith(prefijo))
|
|
66
|
+
throw new Error(`entry outside the package: ${nombre}`);
|
|
67
|
+
const ruta = nombre.slice(prefijo.length);
|
|
68
|
+
if (!rutaSegura(ruta))
|
|
69
|
+
throw new Error(`unsafe path: ${nombre}`);
|
|
70
|
+
bytes += datos.length;
|
|
71
|
+
if (salida.length >= maxFicheros || bytes > maxBytes)
|
|
72
|
+
throw new Error('package too large');
|
|
73
|
+
salida.push({ ruta, contenido: Buffer.from(datos) });
|
|
74
|
+
}
|
|
75
|
+
return salida;
|
|
76
|
+
}
|
|
77
|
+
export function rutaSegura(ruta) {
|
|
78
|
+
if (!ruta || ruta.includes('\\') || ruta.includes('\0') || ruta.startsWith('/'))
|
|
79
|
+
return false;
|
|
80
|
+
if (/^[A-Za-z]:/.test(ruta))
|
|
81
|
+
return false;
|
|
82
|
+
const partes = ruta.split('/');
|
|
83
|
+
return partes.every((p) => p && p !== '.' && p !== '..');
|
|
84
|
+
}
|
|
85
|
+
export function leerTgz(tgz, limites = {}) {
|
|
86
|
+
const maxBytes = limites.maxBytes ?? 20 * 1024 * 1024;
|
|
87
|
+
const tar = gunzipSync(tgz, { maxOutputLength: maxBytes + 2 * 1024 * 1024 });
|
|
88
|
+
return leerTar(tar, limites);
|
|
89
|
+
}
|
|
90
|
+
export function escribirFicheros(ficheros, destino) {
|
|
91
|
+
const base = normalize(destino + sep);
|
|
92
|
+
for (const f of ficheros) {
|
|
93
|
+
const ruta = normalize(join(destino, ...f.ruta.split('/')));
|
|
94
|
+
if (!ruta.startsWith(base))
|
|
95
|
+
throw new Error(`unsafe path: ${f.ruta}`);
|
|
96
|
+
mkdirSync(dirname(ruta), { recursive: true });
|
|
97
|
+
writeFileSync(ruta, f.contenido, { mode: f.ruta.endsWith('.js') ? 0o755 : 0o644 });
|
|
98
|
+
}
|
|
99
|
+
}
|