@glassnote/client 2.4.10 → 2.4.12
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/INSTALACION.md +84 -0
- package/bin/glassnote.js +205 -4
- package/linuxCursor.js +187 -0
- package/main.js +476 -153
- package/package.json +7 -3
- package/pairing.js +338 -0
- package/rendererPaths.js +57 -1
package/INSTALACION.md
CHANGED
|
@@ -41,6 +41,7 @@ que es justo lo que hace `glassnote uninstall`.
|
|
|
41
41
|
| `glassnote start --detach` | lo arranca y libera la terminal |
|
|
42
42
|
| `glassnote install [version]` | instala o reinstala (`latest` por defecto) |
|
|
43
43
|
| `glassnote update [version]` | actualiza a la última publicada |
|
|
44
|
+
| `glassnote pair` | da de alta el equipo con la API key de la organización |
|
|
44
45
|
| `glassnote uninstall` | quita autoarranque, `~/.glassnote` y la línea del rc |
|
|
45
46
|
| `glassnote status` | versión, modo, rutas, autoarranque y estado del sandbox |
|
|
46
47
|
| `glassnote fix-sandbox` | (Linux) restaura el sandbox de Chromium; pide sudo una vez |
|
|
@@ -48,6 +49,89 @@ que es justo lo que hace `glassnote uninstall`.
|
|
|
48
49
|
Banderas de `install`: `--no-path` (no toca el rc, imprime la línea) y `--no-start`
|
|
49
50
|
(instala y no arranca).
|
|
50
51
|
|
|
52
|
+
## Alta masiva: emparejar sin que nadie mire la pantalla
|
|
53
|
+
|
|
54
|
+
El emparejamiento normal es a dos manos: el cliente muestra un código y una persona lo
|
|
55
|
+
escribe en la web. Para un equipo está bien. Para trescientos —una cadena que abre
|
|
56
|
+
locales, un parque que se reinstala entero— es una tarde entera y un error de tipeo cada
|
|
57
|
+
veinte equipos.
|
|
58
|
+
|
|
59
|
+
Con la **API key de la organización**, el equipo se da de alta solo durante la
|
|
60
|
+
instalación. La API contesta con el servidor y el token, el instalador los deja escritos
|
|
61
|
+
donde la app los busca, y el cliente arranca ya conectado.
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
# Linux / macOS — ojo con el `-s --`, es lo que hace que sh le pase las banderas al script
|
|
65
|
+
curl -fsSL https://glassnote.intermark.ec/install.sh | sh -s -- \
|
|
66
|
+
--org-id 55 --api-key <clave> --device-name "Caja 01" --user-name "Santiago"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```powershell
|
|
70
|
+
# Windows
|
|
71
|
+
& ([scriptblock]::Create((irm https://glassnote.intermark.ec/install.ps1))) `
|
|
72
|
+
-OrgId 55 -ApiKey <clave> -DeviceName 'Caja 01' -UserName 'Santiago'
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
| bandera | variable de entorno | por defecto |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `--org-id` | `GLASSNOTE_ORG_ID` | — (obligatoria) |
|
|
78
|
+
| `--api-key` | `GLASSNOTE_API_KEY` | — (obligatoria) |
|
|
79
|
+
| `--device-name` | `GLASSNOTE_DEVICE_NAME` | el hostname del equipo |
|
|
80
|
+
| `--user-name` | `GLASSNOTE_USER_NAME` | el usuario del sistema |
|
|
81
|
+
| `--api-url` | `GLASSNOTE_API_URL` | `https://glassnoteapi.intermark.ec` |
|
|
82
|
+
| `--ws-url` | `GLASSNOTE_WS_URL` | el que anuncie la API |
|
|
83
|
+
|
|
84
|
+
Las variables de entorno existen porque un despliegue por GPO o por MDM muchas veces no
|
|
85
|
+
puede pasar argumentos pero sí poner entorno.
|
|
86
|
+
|
|
87
|
+
### Otro servidor
|
|
88
|
+
|
|
89
|
+
**El websocket no se elige en el instalador: lo decide la API que atiende el alta.** El
|
|
90
|
+
token que emite sólo vale en su propio websocket, así que apuntar a otro sitio no daría
|
|
91
|
+
un equipo conectado a otro servidor, daría un equipo dado de alta que no conecta. Para
|
|
92
|
+
instalar contra otra API se cambia `--api-url` y el websocket viene de vuelta en la
|
|
93
|
+
respuesta:
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
curl -fsSL https://glassnote.intermark.ec/install.sh | sh -s -- \
|
|
97
|
+
--api-url https://api.otrocliente.com --org-id 12 --api-key <clave>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Esa URL de websocket es además de donde el cliente saca el origen HTTP de la API para
|
|
101
|
+
reportar su versión, así que es **el único dato de servidor** que el equipo guarda.
|
|
102
|
+
|
|
103
|
+
`--ws-url` cubre el único caso que lo anterior no: que la API anuncie una dirección a la
|
|
104
|
+
que el equipo no llega —una IP de LAN detrás de un NAT, un proxy inverso publicado con
|
|
105
|
+
otro nombre, DNS partido—. Ahí el alta sale bien y los equipos guardan una URL muerta sin
|
|
106
|
+
ninguna señal en el momento de instalar. Por eso, cuando el host que anuncia la API no es
|
|
107
|
+
el mismo al que se pidió el alta, el instalador lo dice en pantalla. **Lo correcto es
|
|
108
|
+
arreglar `WEBSOCKET_URL` en la API**; `--ws-url` es la salida cuando no se puede.
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
glassnote pair --org-id 12 --api-key <clave> \
|
|
112
|
+
--api-url https://api.cliente.local --ws-url wss://glassnote.cliente.com/ws
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**El nombre por defecto es el del propio equipo, no uno genérico.** Es lo que hace que el
|
|
116
|
+
*mismo* comando sirva para todo el parque: sin eso, el tablero termina con trescientos
|
|
117
|
+
"Unnamed Device" y hay que renombrarlos a mano, que es justo el trabajo que esto viene a
|
|
118
|
+
evitar.
|
|
119
|
+
|
|
120
|
+
La API key se saca en la web, en las claves de la organización. **Da de alta equipos en
|
|
121
|
+
esa organización**: es un secreto de despliegue, no algo que se reparta. Caduca a los 30
|
|
122
|
+
días, lo que acota lo que se puede hacer con una que se filtre.
|
|
123
|
+
|
|
124
|
+
Un equipo ya instalado se empareja sin reinstalar:
|
|
125
|
+
|
|
126
|
+
```sh
|
|
127
|
+
glassnote pair --org-id 55 --api-key <clave> --device-name "Caja 01"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Si el alta falla, el comando **sale con código distinto de cero** y dice por qué (la
|
|
131
|
+
clave no sirve, la organización no es la de esa clave, el plan llegó a su tope de
|
|
132
|
+
equipos). El cliente queda instalado igual: se reintenta con `glassnote pair`. Emparejar
|
|
133
|
+
dos veces el mismo equipo no lo duplica ni gasta un cupo extra; solo le renueva el token.
|
|
134
|
+
|
|
51
135
|
`install` y `update` también aceptan un *spec* de npm en vez de una versión: un `.tgz`,
|
|
52
136
|
una ruta local o una URL. Es lo que permite probar un paquete antes de publicarlo:
|
|
53
137
|
|
package/bin/glassnote.js
CHANGED
|
@@ -17,6 +17,7 @@ const os = require('os');
|
|
|
17
17
|
const { spawn, spawnSync } = require('child_process');
|
|
18
18
|
|
|
19
19
|
const mode = require('../npmMode');
|
|
20
|
+
const pairing = require('../pairing');
|
|
20
21
|
const pkg = require('../package.json');
|
|
21
22
|
|
|
22
23
|
const IS_WIN = process.platform === 'win32';
|
|
@@ -140,6 +141,78 @@ function esperarAQueCierre(maxMs) {
|
|
|
140
141
|
return false;
|
|
141
142
|
}
|
|
142
143
|
|
|
144
|
+
// Windows no deja reemplazar los archivos de un proceso vivo. Reinstalar encima del
|
|
145
|
+
// cliente abierto muere con «EBUSY: resource busy or locked, rename ...\electron\dist\
|
|
146
|
+
// icudtl.dat», y muere A MITAD: npm ya apartó parte del árbol viejo, así que la
|
|
147
|
+
// instalación que había queda rota además de no haber instalado la nueva. Es el fallo
|
|
148
|
+
// que se lleva puesto a cualquiera que corra el `irm | iex` una segunda vez, que es
|
|
149
|
+
// justo lo que se le pide a la gente para actualizar.
|
|
150
|
+
//
|
|
151
|
+
// Se cierra por RUTA del ejecutable, nunca por nombre: "electron.exe" es también el
|
|
152
|
+
// VS Code y el Slack de quien esté delante, y cerrarle eso para instalar esto sería
|
|
153
|
+
// bastante peor que el fallo que se viene a arreglar. Sólo se toca lo que vive dentro
|
|
154
|
+
// de ~/.glassnote, que es nuestro por definición.
|
|
155
|
+
// El guion de PowerShell, aparte: decidir QUE se le pide al sistema es una funcion pura
|
|
156
|
+
// y se puede comprobar desde cualquier maquina; matar procesos de verdad, no. Y la parte
|
|
157
|
+
// que mas silenciosamente se rompe es justo esta, el entrecomillado de la ruta.
|
|
158
|
+
//
|
|
159
|
+
// Comillas SIMPLES de PowerShell: son literales y no interpretan la barra invertida. Con
|
|
160
|
+
// comillas dobles habria que escapar, y una ruta escapada a la manera de JSON le llega a
|
|
161
|
+
// PowerShell con las barras duplicadas y no coincide con nada: el filtro no encontraria
|
|
162
|
+
// un solo proceso y el EBUSY volveria, mudo.
|
|
163
|
+
//
|
|
164
|
+
// El filtro apunta a `<home>\npm\node_modules`, NO a `<home>` entero. La diferencia no es
|
|
165
|
+
// cosmetica: cuando el equipo no tenia Node, el instalador se baja el suyo a
|
|
166
|
+
// `<home>\node-v20.18.1-win-x64\node.exe` —que es el caso normal en Windows— y ese
|
|
167
|
+
// node.exe es el que esta ejecutando ESTE codigo. Filtrando por `<home>` el instalador se
|
|
168
|
+
// mataba a si mismo a mitad, salia con codigo -1 y no instalaba nada. Se vio en la VM de
|
|
169
|
+
// Windows el 2026-09-07; en Linux no pasa porque alli no hay nada que cerrar.
|
|
170
|
+
//
|
|
171
|
+
// Se excluye ademas el pid propio, por si algun dia el CLI viviera dentro de esa carpeta.
|
|
172
|
+
// Donde viven los binarios del cliente YA INSTALADO. Es lo unico que npm va a
|
|
173
|
+
// reemplazar, y por tanto lo unico que hay que cerrar.
|
|
174
|
+
function rutaDelClienteInstalado() {
|
|
175
|
+
return path.join(mode.NPM_PREFIX, 'node_modules');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function guionCerrarCliente(raizDir, pidPropio) {
|
|
179
|
+
const raiz = `'${String(raizDir).replace(/'/g, "''")}'`;
|
|
180
|
+
const pid = Number(pidPropio) || 0;
|
|
181
|
+
return [
|
|
182
|
+
`$raiz = ${raiz}`,
|
|
183
|
+
`$yo = ${pid}`,
|
|
184
|
+
'$procs = @(Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -ne $yo -and $_.ExecutablePath -and $_.ExecutablePath.StartsWith($raiz, [System.StringComparison]::OrdinalIgnoreCase) })',
|
|
185
|
+
'$procs | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }',
|
|
186
|
+
'Write-Output $procs.Count',
|
|
187
|
+
].join('; ');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function cerrarClienteWindows() {
|
|
191
|
+
if (!IS_WIN) return true; // en POSIX se puede reemplazar un archivo abierto
|
|
192
|
+
|
|
193
|
+
const res = spawnSync(
|
|
194
|
+
'powershell.exe',
|
|
195
|
+
['-NoProfile', '-NonInteractive', '-Command', guionCerrarCliente(rutaDelClienteInstalado(), process.pid)],
|
|
196
|
+
{ encoding: 'utf8', timeout: 20000 }
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// Si PowerShell no está o falló, no se sigue a ciegas: se mira si el cliente
|
|
200
|
+
// contesta, que es la señal barata de que sigue vivo.
|
|
201
|
+
if (res.error || res.status !== 0) {
|
|
202
|
+
if (!clienteEnMarcha()) return true;
|
|
203
|
+
console.error('glassnote: no pude cerrar el cliente automáticamente.');
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const cerrados = parseInt(String(res.stdout || '').trim(), 10) || 0;
|
|
208
|
+
if (cerrados === 0) return true;
|
|
209
|
+
|
|
210
|
+
step(`el cliente estaba abierto: cerrados ${cerrados} procesos suyos`);
|
|
211
|
+
// El proceso desaparece de la lista antes de que el SO suelte los handles de sus
|
|
212
|
+
// archivos, y npm necesita los handles, no la lista.
|
|
213
|
+
return esperarAQueCierre(15000);
|
|
214
|
+
}
|
|
215
|
+
|
|
143
216
|
const log = (...args) => console.log(...args);
|
|
144
217
|
// Cada paso se dice EN CUANTO EMPIEZA: lo que hace que un instalador parezca colgado
|
|
145
218
|
// no es que tarde, es que tarde sin decir en qué está.
|
|
@@ -464,16 +537,29 @@ function borrarAppMac() {
|
|
|
464
537
|
}
|
|
465
538
|
}
|
|
466
539
|
|
|
467
|
-
function cmdInstall(args) {
|
|
468
|
-
|
|
540
|
+
async function cmdInstall(args) {
|
|
541
|
+
// El argumento suelto es la version, pero las banderas de emparejamiento traen valor
|
|
542
|
+
// propio: sin saltarlo, `install --api-key abc` intentaba instalar la version "abc".
|
|
543
|
+
const version = pairing.argumentoPosicional(args) || 'latest';
|
|
469
544
|
const noPath = args.includes('--no-path');
|
|
470
545
|
const noStart = args.includes('--no-start');
|
|
546
|
+
const emparejamiento = pairing.leerOpciones(args, process.env);
|
|
471
547
|
|
|
472
548
|
const spec = toSpec(version);
|
|
473
549
|
log(`glassnote — instalando ${spec}`);
|
|
474
550
|
fs.mkdirSync(mode.NPM_PREFIX, { recursive: true });
|
|
475
551
|
fs.mkdirSync(mode.BIN_DIR, { recursive: true });
|
|
476
552
|
|
|
553
|
+
// ANTES de que npm mueva un solo archivo: si el cliente está abierto, npm apartaría
|
|
554
|
+
// el árbol viejo a medias y dejaría rota la instalación que ya funcionaba.
|
|
555
|
+
if (!cerrarClienteWindows()) {
|
|
556
|
+
fail(
|
|
557
|
+
'el cliente sigue abierto y Windows no deja reemplazar sus archivos.\n' +
|
|
558
|
+
' Cerralo desde el ícono de la bandeja (clic derecho → Salir) y volvé a correr esto.\n' +
|
|
559
|
+
' No se tocó nada: la instalación que tenías sigue como estaba.'
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
477
563
|
// Los scripts de instalación van ACTIVADOS a propósito: el binario de electron se
|
|
478
564
|
// baja en un postinstall. Con --ignore-scripts quedaría instalado y roto.
|
|
479
565
|
step(`npm install -g --prefix ${mode.NPM_PREFIX} (sin root, la primera vez tarda: baja electron)`);
|
|
@@ -530,6 +616,14 @@ function cmdInstall(args) {
|
|
|
530
616
|
log('Para actualizar más adelante: glassnote update (o correr esto mismo otra vez).');
|
|
531
617
|
log('');
|
|
532
618
|
|
|
619
|
+
// Antes de arrancar: si la instalacion trae credenciales de organizacion, el equipo se
|
|
620
|
+
// da de alta ahora. Arrancarlo primero solo consigue que aparezca la pantalla del
|
|
621
|
+
// keycode un instante antes de quedar emparejado igual.
|
|
622
|
+
if (pairing.pidenEmparejar(emparejamiento)) {
|
|
623
|
+
const codigo = await emparejar(emparejamiento, { reciénInstalado: true });
|
|
624
|
+
if (codigo !== 0) return codigo;
|
|
625
|
+
}
|
|
626
|
+
|
|
533
627
|
if (noStart) {
|
|
534
628
|
log('Arrancalo cuando quieras: glassnote');
|
|
535
629
|
return 0;
|
|
@@ -549,6 +643,66 @@ function cmdInstall(args) {
|
|
|
549
643
|
return 0;
|
|
550
644
|
}
|
|
551
645
|
|
|
646
|
+
// Alta desatendida contra la API. Devuelve codigo de salida y no lanza: un despliegue
|
|
647
|
+
// masivo se guia por el codigo, no por lo que aparezca en pantalla.
|
|
648
|
+
async function emparejar(opciones, { reciénInstalado = false } = {}) {
|
|
649
|
+
const userData = conUserData((u) => u);
|
|
650
|
+
if (!userData) {
|
|
651
|
+
console.error('glassnote: no pude leer los datos de usuario para emparejar.');
|
|
652
|
+
return 1;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
step(`dando de alta el equipo en la organizacion ${opciones.orgId || '(sin id)'}`);
|
|
656
|
+
|
|
657
|
+
try {
|
|
658
|
+
const alta = await pairing.emparejar(opciones, userData);
|
|
659
|
+
log('');
|
|
660
|
+
log(
|
|
661
|
+
alta.yaEstaba
|
|
662
|
+
? 'El equipo ya estaba en la organizacion; se le renovo el token.'
|
|
663
|
+
: 'Equipo dado de alta.'
|
|
664
|
+
);
|
|
665
|
+
log(` nombre: ${alta.name}`);
|
|
666
|
+
log(` organizacion: ${alta.organizationId}`);
|
|
667
|
+
log(` servidor: ${alta.server}${alta.servidorForzado ? ' (forzado con --ws-url)' : ''}`);
|
|
668
|
+
log(` uuid: ${alta.uuid}`);
|
|
669
|
+
log('');
|
|
670
|
+
// El equipo va a hablar SOLO con este websocket, y de él saca también el origen
|
|
671
|
+
// HTTP de la API. Si no es el mismo host al que se acaba de pedir el alta, la API
|
|
672
|
+
// está anunciando otra dirección: puede ser a propósito, o puede ser un
|
|
673
|
+
// WEBSOCKET_URL mal puesto que deja al parque entero conectando a la nada. Se
|
|
674
|
+
// dice acá, que es el único momento en que alguien está mirando.
|
|
675
|
+
if (!alta.mismoOrigenQueLaApi) {
|
|
676
|
+
log(`Ojo: la API contestó que el equipo se conecte a ${alta.servidorAnunciado || alta.server},`);
|
|
677
|
+
log('que no es el host al que le pediste el alta. Si el equipo no llega ahí, se');
|
|
678
|
+
log('arregla WEBSOCKET_URL en la API, o se fuerza acá con --ws-url.');
|
|
679
|
+
log('');
|
|
680
|
+
}
|
|
681
|
+
return 0;
|
|
682
|
+
} catch (error) {
|
|
683
|
+
console.error('');
|
|
684
|
+
console.error(`glassnote: no se pudo dar de alta el equipo: ${error.message}`);
|
|
685
|
+
if (reciénInstalado) {
|
|
686
|
+
console.error('El cliente quedó instalado igual. Cuando esto se resuelva:');
|
|
687
|
+
} else {
|
|
688
|
+
console.error('El equipo sigue como estaba. Para reintentar:');
|
|
689
|
+
}
|
|
690
|
+
console.error(' glassnote pair --org-id <id> --api-key <clave> --device-name <nombre>');
|
|
691
|
+
console.error('');
|
|
692
|
+
return 1;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
async function cmdPair(args) {
|
|
697
|
+
const opciones = pairing.leerOpciones(args, process.env);
|
|
698
|
+
if (!pairing.pidenEmparejar(opciones)) {
|
|
699
|
+
console.error('glassnote pair: falta --api-key (o la variable GLASSNOTE_API_KEY).\n');
|
|
700
|
+
cmdHelp();
|
|
701
|
+
return 2;
|
|
702
|
+
}
|
|
703
|
+
return emparejar(opciones);
|
|
704
|
+
}
|
|
705
|
+
|
|
552
706
|
function cmdUpdate(args) {
|
|
553
707
|
if (mode.isEphemeral()) {
|
|
554
708
|
log('Estás corriendo por npx: npx ya baja la última versión cada vez.');
|
|
@@ -571,6 +725,16 @@ function cmdUpdate(args) {
|
|
|
571
725
|
|
|
572
726
|
if (waitMs > 0) esperarAQueCierre(waitMs);
|
|
573
727
|
|
|
728
|
+
// Sin `--wait` esto lo está corriendo una persona a mano, con el cliente abierto: el
|
|
729
|
+
// mismo EBUSY de Windows que en install, y con el mismo remedio.
|
|
730
|
+
else if (!cerrarClienteWindows()) {
|
|
731
|
+
fail(
|
|
732
|
+
'el cliente sigue abierto y Windows no deja reemplazar sus archivos.\n' +
|
|
733
|
+
' Cerralo desde el ícono de la bandeja (clic derecho → Salir) y volvé a intentar.\n' +
|
|
734
|
+
' No se tocó nada: la versión que tenías sigue como estaba.'
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
|
|
574
738
|
// El cwd no puede estar dentro de lo que npm va a borrar: en Windows un directorio
|
|
575
739
|
// que es el cwd de un proceso vivo no se puede eliminar, y ese proceso es este.
|
|
576
740
|
try {
|
|
@@ -731,6 +895,7 @@ function cmdHelp() {
|
|
|
731
895
|
glassnote arranca el cliente
|
|
732
896
|
glassnote start --detach lo arranca y libera la terminal
|
|
733
897
|
glassnote update [version] actualiza (por defecto, la última)
|
|
898
|
+
glassnote pair da de alta el equipo con la API key de la organización
|
|
734
899
|
glassnote uninstall quita autoarranque, ~/.glassnote y la línea del rc
|
|
735
900
|
glassnote status versión, modo, rutas y estado del autoarranque
|
|
736
901
|
glassnote fix-sandbox (Linux) restaura el sandbox de Chromium; pide sudo una vez
|
|
@@ -738,6 +903,26 @@ function cmdHelp() {
|
|
|
738
903
|
Banderas de install:
|
|
739
904
|
--no-path no toca el rc; imprime la línea para pegarla
|
|
740
905
|
--no-start instala y no arranca el cliente
|
|
906
|
+
|
|
907
|
+
Emparejamiento desatendido (install y pair) — para dar de alta muchos equipos sin que
|
|
908
|
+
nadie escriba el código en la web:
|
|
909
|
+
--org-id <id> organización a la que entra el equipo
|
|
910
|
+
--api-key <clave> API key de esa organización
|
|
911
|
+
--device-name <n> nombre del equipo (por defecto, el hostname)
|
|
912
|
+
--user-name <n> persona a cargo (por defecto, el usuario del sistema)
|
|
913
|
+
--api-url <url> otra API (por defecto, ${pairing.API_URL_POR_DEFECTO})
|
|
914
|
+
--ws-url <url> fuerza el websocket que guarda el equipo
|
|
915
|
+
|
|
916
|
+
El websocket NO se elige acá: lo decide la API que atiende el alta, porque el token que
|
|
917
|
+
emite sólo vale en el suyo. Para dar de alta contra otro servidor se cambia --api-url y el
|
|
918
|
+
websocket viene de vuelta solo. --ws-url es para el caso en que la API anuncia una
|
|
919
|
+
dirección a la que el equipo no llega (IP de LAN, proxy inverso con otro nombre).
|
|
920
|
+
|
|
921
|
+
Las mismas se pueden pasar por entorno, para despliegues que no admiten argumentos:
|
|
922
|
+
GLASSNOTE_ORG_ID, GLASSNOTE_API_KEY, GLASSNOTE_DEVICE_NAME, GLASSNOTE_USER_NAME,
|
|
923
|
+
GLASSNOTE_API_URL, GLASSNOTE_WS_URL.
|
|
924
|
+
|
|
925
|
+
npx @glassnote/client install --org-id 55 --api-key abc123 --device-name "Caja 01"
|
|
741
926
|
`);
|
|
742
927
|
return 0;
|
|
743
928
|
}
|
|
@@ -762,6 +947,9 @@ function main() {
|
|
|
762
947
|
});
|
|
763
948
|
case 'install':
|
|
764
949
|
return cmdInstall(rest);
|
|
950
|
+
case 'pair':
|
|
951
|
+
case 'enroll':
|
|
952
|
+
return cmdPair(rest);
|
|
765
953
|
case 'update':
|
|
766
954
|
case 'upgrade':
|
|
767
955
|
return cmdUpdate(rest);
|
|
@@ -787,7 +975,20 @@ function main() {
|
|
|
787
975
|
// Requerido desde un test, el archivo NO debe correr nada: solo expone las piezas que
|
|
788
976
|
// se comprueban. Como CLI se comporta igual que siempre.
|
|
789
977
|
if (require.main === module) {
|
|
790
|
-
|
|
978
|
+
// `install` y `pair` hablan con la API, asi que main() puede devolver una promesa.
|
|
979
|
+
// Salir con process.exit() sin esperarla cortaria el alta a la mitad.
|
|
980
|
+
const resultado = main();
|
|
981
|
+
if (resultado && typeof resultado.then === 'function') {
|
|
982
|
+
resultado.then(
|
|
983
|
+
(codigo) => process.exit(codigo),
|
|
984
|
+
(error) => {
|
|
985
|
+
console.error(`glassnote: ${error && error.message ? error.message : error}`);
|
|
986
|
+
process.exit(1);
|
|
987
|
+
}
|
|
988
|
+
);
|
|
989
|
+
} else {
|
|
990
|
+
process.exit(resultado);
|
|
991
|
+
}
|
|
791
992
|
} else {
|
|
792
|
-
module.exports = { npmInvocation, npmEnv, toSpec };
|
|
993
|
+
module.exports = { npmInvocation, npmEnv, toSpec, guionCerrarCliente, cerrarClienteWindows };
|
|
793
994
|
}
|
package/linuxCursor.js
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// Donde esta el mouse, preguntado directo al servidor X.
|
|
2
|
+
//
|
|
3
|
+
// Hace falta porque screen.getCursorScreenPoint() de Electron miente justo en el unico caso
|
|
4
|
+
// que le importa a un overlay: mientras la ventana ignora el mouse (input shape de 1x1 px en
|
|
5
|
+
// X11) Chromium no recibe un solo evento de puntero, y esa funcion devuelve para siempre la
|
|
6
|
+
// ultima posicion que alcanzo a ver. Medido:
|
|
7
|
+
//
|
|
8
|
+
// ventana ignorando -> sigue al mouse (cae al XQueryPointer del servidor)
|
|
9
|
+
// ventana clickeable -> sigue al mouse (le llegan eventos reales)
|
|
10
|
+
// vuelve a ignorar -> CONGELADA en el ultimo punto que vio, para siempre
|
|
11
|
+
//
|
|
12
|
+
// O sea: funciona hasta la primera interaccion y despues miente. Ese es exactamente el
|
|
13
|
+
// "andaba y dejo de andar" del passthrough en Linux.
|
|
14
|
+
//
|
|
15
|
+
// Asi que se le pregunta al servidor: XQueryPointer, opcode 38. Es protocolo binario a mano,
|
|
16
|
+
// pero es protocolo congelado hace decadas y no arrastra ninguna dependencia nativa —que en
|
|
17
|
+
// este cliente, que se instala con npx y sin compilador, no es un lujo sino un requisito—.
|
|
18
|
+
// Si algo falla (Wayland, display por TCP, sin cookie) se devuelve null y quien llama se las
|
|
19
|
+
// arregla con lo que tenga.
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const net = require('net');
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const os = require('os');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
|
|
27
|
+
const OPCODE_QUERY_POINTER = 38;
|
|
28
|
+
|
|
29
|
+
let socket = null;
|
|
30
|
+
let ventanaRaiz = null;
|
|
31
|
+
let ultimaPosicion = null;
|
|
32
|
+
let consultaEnVuelo = false;
|
|
33
|
+
let inutilizable = false;
|
|
34
|
+
|
|
35
|
+
// El .Xauthority es una lista de bloques con largo de 2 bytes big-endian: familia, direccion,
|
|
36
|
+
// numero de display, nombre del metodo y la cookie. Solo interesa la MIT-MAGIC-COOKIE-1 de
|
|
37
|
+
// este display (o la que no fija display, que vale para todos).
|
|
38
|
+
function leerCookie(numeroDisplay) {
|
|
39
|
+
const archivo = process.env.XAUTHORITY || path.join(os.homedir(), '.Xauthority');
|
|
40
|
+
let buf;
|
|
41
|
+
try {
|
|
42
|
+
buf = fs.readFileSync(archivo);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let i = 0;
|
|
48
|
+
const bloque = () => {
|
|
49
|
+
if (i + 2 > buf.length) return null;
|
|
50
|
+
const largo = buf.readUInt16BE(i);
|
|
51
|
+
i += 2;
|
|
52
|
+
if (i + largo > buf.length) return null;
|
|
53
|
+
const datos = buf.subarray(i, i + largo);
|
|
54
|
+
i += largo;
|
|
55
|
+
return datos;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
while (i + 2 <= buf.length) {
|
|
59
|
+
i += 2; // familia
|
|
60
|
+
const direccion = bloque();
|
|
61
|
+
const numero = bloque();
|
|
62
|
+
const nombre = bloque();
|
|
63
|
+
const cookie = bloque();
|
|
64
|
+
if (!direccion || !numero || !nombre || !cookie) break;
|
|
65
|
+
if (nombre.toString() !== 'MIT-MAGIC-COOKIE-1') continue;
|
|
66
|
+
const suDisplay = numero.toString();
|
|
67
|
+
if (suDisplay === '' || suDisplay === String(numeroDisplay)) return { nombre, cookie };
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const relleno = (largo) => (4 - (largo % 4)) % 4;
|
|
73
|
+
|
|
74
|
+
function numeroDeDisplay() {
|
|
75
|
+
const display = process.env.DISPLAY || '';
|
|
76
|
+
// Solo sockets unix locales: ":0", ":0.0". Un display por TCP ("host:0") no se atiende.
|
|
77
|
+
const coincide = /^:(\d+)/.exec(display);
|
|
78
|
+
return coincide ? parseInt(coincide[1], 10) : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function conectar() {
|
|
82
|
+
const numero = numeroDeDisplay();
|
|
83
|
+
if (numero === null) {
|
|
84
|
+
inutilizable = true;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const auth = leerCookie(numero) || { nombre: Buffer.alloc(0), cookie: Buffer.alloc(0) };
|
|
89
|
+
let cliente;
|
|
90
|
+
try {
|
|
91
|
+
cliente = net.connect(`/tmp/.X11-unix/X${numero}`);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
inutilizable = true;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
cliente.on('error', () => {
|
|
98
|
+
inutilizable = true;
|
|
99
|
+
socket = null;
|
|
100
|
+
ventanaRaiz = null;
|
|
101
|
+
});
|
|
102
|
+
cliente.on('close', () => {
|
|
103
|
+
socket = null;
|
|
104
|
+
ventanaRaiz = null;
|
|
105
|
+
consultaEnVuelo = false;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
cliente.once('connect', () => {
|
|
109
|
+
const largoNombre = auth.nombre.length;
|
|
110
|
+
const largoCookie = auth.cookie.length;
|
|
111
|
+
const inicio = Buffer.alloc(
|
|
112
|
+
12 + largoNombre + relleno(largoNombre) + largoCookie + relleno(largoCookie)
|
|
113
|
+
);
|
|
114
|
+
inicio.write('l', 0, 'ascii'); // little endian
|
|
115
|
+
inicio.writeUInt16LE(11, 2); // protocolo 11.0
|
|
116
|
+
inicio.writeUInt16LE(0, 4);
|
|
117
|
+
inicio.writeUInt16LE(largoNombre, 6);
|
|
118
|
+
inicio.writeUInt16LE(largoCookie, 8);
|
|
119
|
+
auth.nombre.copy(inicio, 12);
|
|
120
|
+
auth.cookie.copy(inicio, 12 + largoNombre + relleno(largoNombre));
|
|
121
|
+
cliente.write(inicio);
|
|
122
|
+
|
|
123
|
+
cliente.once('data', (respuesta) => {
|
|
124
|
+
if (respuesta[0] !== 1) {
|
|
125
|
+
// 0 = rechazado (cookie mala), 2 = pide mas autenticacion.
|
|
126
|
+
inutilizable = true;
|
|
127
|
+
cliente.destroy();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// La ventana raiz esta despues del vendor y de los formatos de pixel, todos de
|
|
131
|
+
// largo variable: hay que saltearlos para llegar a la primera pantalla.
|
|
132
|
+
const largoVendor = respuesta.readUInt16LE(24);
|
|
133
|
+
const cantidadFormatos = respuesta[29];
|
|
134
|
+
const inicioPantallas = 40 + largoVendor + relleno(largoVendor) + 8 * cantidadFormatos;
|
|
135
|
+
if (inicioPantallas + 4 > respuesta.length) {
|
|
136
|
+
inutilizable = true;
|
|
137
|
+
cliente.destroy();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
ventanaRaiz = respuesta.readUInt32LE(inicioPantallas);
|
|
141
|
+
socket = cliente;
|
|
142
|
+
|
|
143
|
+
cliente.on('data', (paquete) => {
|
|
144
|
+
// Respuesta de QueryPointer: 32 bytes, root_x y root_y en 20 y 22.
|
|
145
|
+
if (paquete.length >= 32 && paquete[0] === 1) {
|
|
146
|
+
ultimaPosicion = { x: paquete.readInt16LE(20), y: paquete.readInt16LE(22) };
|
|
147
|
+
}
|
|
148
|
+
consultaEnVuelo = false;
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Pide la posicion al servidor y devuelve la ULTIMA que contesto: la respuesta llega por el
|
|
155
|
+
// socket, no en el acto. Consultando a 30 Hz eso son 30 ms de atraso, que para decidir si el
|
|
156
|
+
// cursor esta sobre un boton no se nota. Nunca hay mas de una consulta en vuelo: si el
|
|
157
|
+
// servidor se atrasa, no se le encima una cola de pedidos que ya no le sirven a nadie.
|
|
158
|
+
function posicion() {
|
|
159
|
+
if (inutilizable) return null;
|
|
160
|
+
if (!socket) {
|
|
161
|
+
conectar();
|
|
162
|
+
return ultimaPosicion;
|
|
163
|
+
}
|
|
164
|
+
if (!consultaEnVuelo) {
|
|
165
|
+
const pedido = Buffer.alloc(8);
|
|
166
|
+
pedido[0] = OPCODE_QUERY_POINTER;
|
|
167
|
+
pedido.writeUInt16LE(2, 2); // largo en palabras de 4 bytes
|
|
168
|
+
pedido.writeUInt32LE(ventanaRaiz, 4);
|
|
169
|
+
consultaEnVuelo = true;
|
|
170
|
+
try {
|
|
171
|
+
socket.write(pedido);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
consultaEnVuelo = false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return ultimaPosicion;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function detener() {
|
|
180
|
+
if (socket) socket.destroy();
|
|
181
|
+
socket = null;
|
|
182
|
+
ventanaRaiz = null;
|
|
183
|
+
ultimaPosicion = null;
|
|
184
|
+
consultaEnVuelo = false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = { posicion, detener };
|