feat(cras-install): actualizar sin privilegios cuando no hay root ni sudo -n
En entornos donde no se usa root, ninguna de las dos vías que había servía: la elevación abortaba con 409 y `user-service` exige mover la instalación al home, que en un servidor con el agente ya instalado en /opt pide un paso privilegiado para retirar el unit viejo. Se agrega una tercera vía para ACTUALIZACIONES: dejar el unit como está y solo reemplazar el binario, que es lo único que una actualización necesita. Corre `install.sh --update-in-place` sin prefijo de elevación. Tres precondiciones, comprobadas por SSH ANTES de subir 270 MB, cada una con su propio motivo de rechazo porque cada una tiene un remedio distinto: - El directorio de instalación debe ser escribible por la cuenta SSH. Es el único permiso que hace falta: `install` desvincula el destino antes de crearlo, así que un binario en ejecución no es obstáculo (eso es cosa de `cp`). - El unit debe correr con ese mismo usuario. Si quedó con User=root —alguien instaló desde un `sudo -i`— la cuenta no puede señalizar el proceso. Un User vacío se trata como root, que es lo que hace systemd. - El unit debe tener Restart=always, que es quien vuelve a levantarlo. Sin eso, señalizarlo lo dejaría muerto. Y se rechaza si hay una restauración en curso, aquí y otra vez en el destino. Dos correcciones de robustez en la verificación posterior: - El sello config/.version se sondea en vez de leerse una vez. En esta vía se omite el bootstrap y el sello lo escribe el proceso al reiniciarse, así que durante unos segundos sigue teniendo la versión ANTERIOR: la lectura única convertía una actualización correcta en "reporta 1.0.0, se esperaba 1.1.1". - `systemctl is-active` puede devolver 'activating' justo tras el reinicio, así que la evidencia que manda es el proceso vivo con el binario nuevo. El código 75 (EX_TEMPFAIL) de install.sh se traduce a un 409 con el motivo real —"está restaurando, reintenta"— en vez del 502 genérico que hacía pensar que la instalación se había roto. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
import {
|
import {
|
||||||
execRemote,
|
execRemote,
|
||||||
|
probeInPlaceUpdate,
|
||||||
probeLinuxElevation,
|
probeLinuxElevation,
|
||||||
psEncoded,
|
psEncoded,
|
||||||
shQuote,
|
shQuote,
|
||||||
@@ -294,3 +295,106 @@ describe('probeLinuxElevation', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Precondiciones de la actualización en sitio sin privilegios.
|
||||||
|
*
|
||||||
|
* Cada rechazo tiene un remedio distinto, y por eso no basta un booleano. Lo que se prueba aquí
|
||||||
|
* son los NO: el sí es trivial y el daño está en aceptar un servidor donde no se puede.
|
||||||
|
*/
|
||||||
|
describe('probeInPlaceUpdate', () => {
|
||||||
|
const PREFIX = '/opt/cloudrestoreas';
|
||||||
|
|
||||||
|
function sftpFor(responses: Record<string, { code?: number; stdout?: string; stderr?: string }>) {
|
||||||
|
return fakeSftp((command) => {
|
||||||
|
for (const [needle, result] of Object.entries(responses)) {
|
||||||
|
if (command.includes(needle)) return result;
|
||||||
|
}
|
||||||
|
return { code: 0, stdout: '' };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Destino ideal: ruta del usuario, unit suyo con Restart=always, sin jobs. */
|
||||||
|
const OK = {
|
||||||
|
'test -w': { code: 0, stdout: 'si' },
|
||||||
|
'systemctl show': { code: 0, stdout: 'User=srvmid_db\nRestart=always' },
|
||||||
|
'/Temp': { code: 0, stdout: '' }
|
||||||
|
};
|
||||||
|
|
||||||
|
it('acepta cuando se cumplen las tres condiciones', async () => {
|
||||||
|
const { sftp } = sftpFor(OK);
|
||||||
|
expect(await probeInPlaceUpdate(sftp as never, PREFIX, 'srvmid_db')).toMatchObject({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rechaza si el directorio no es escribible, y dice de quién es', async () => {
|
||||||
|
// Sin escritura en el DIRECTORIO no se puede reemplazar el binario, y es el único
|
||||||
|
// permiso que hace falta: `install` desvincula antes de crear.
|
||||||
|
const { sftp } = sftpFor({
|
||||||
|
...OK,
|
||||||
|
'test -w': { code: 1, stdout: 'no' },
|
||||||
|
'stat -c': { code: 0, stdout: 'root' }
|
||||||
|
});
|
||||||
|
const r = await probeInPlaceUpdate(sftp as never, PREFIX, 'srvmid_db');
|
||||||
|
expect(r.ok).toBe(false);
|
||||||
|
expect(r.reason).toContain('root');
|
||||||
|
expect(r.reason).toContain(PREFIX);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rechaza si el servicio corre como otro usuario', async () => {
|
||||||
|
// Si el unit quedó con User=root —alguien instaló desde un `sudo -i`— la cuenta SSH no
|
||||||
|
// puede señalizar el proceso y toda la vía se cae.
|
||||||
|
const { sftp } = sftpFor({
|
||||||
|
...OK,
|
||||||
|
'systemctl show': { code: 0, stdout: 'User=root\nRestart=always' }
|
||||||
|
});
|
||||||
|
const r = await probeInPlaceUpdate(sftp as never, PREFIX, 'srvmid_db');
|
||||||
|
expect(r.ok).toBe(false);
|
||||||
|
expect(r.reason).toMatch(/corre como 'root'/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('trata el User vacío como root, que es lo que hace systemd', async () => {
|
||||||
|
const { sftp } = sftpFor({
|
||||||
|
...OK,
|
||||||
|
'systemctl show': { code: 0, stdout: 'User=\nRestart=always' }
|
||||||
|
});
|
||||||
|
const r = await probeInPlaceUpdate(sftp as never, PREFIX, 'srvmid_db');
|
||||||
|
expect(r.ok).toBe(false);
|
||||||
|
expect(r.reason).toMatch(/root/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rechaza sin Restart=always: matarlo lo dejaría muerto', async () => {
|
||||||
|
for (const restart of ['on-failure', 'no', '']) {
|
||||||
|
const { sftp } = sftpFor({
|
||||||
|
...OK,
|
||||||
|
'systemctl show': { code: 0, stdout: `User=srvmid_db\nRestart=${restart}` }
|
||||||
|
});
|
||||||
|
const r = await probeInPlaceUpdate(sftp as never, PREFIX, 'srvmid_db');
|
||||||
|
expect(r.ok, `Restart=${restart}`).toBe(false);
|
||||||
|
expect(r.reason).toMatch(/Restart=always/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rechaza si hay una restauración en curso, antes de subir 270 MB', async () => {
|
||||||
|
// El mismo chequeo lo repite install.sh en el destino, pero hacerlo aquí evita la
|
||||||
|
// transferencia completa para nada.
|
||||||
|
const { sftp } = sftpFor({ ...OK, '/Temp': { code: 0, stdout: 'job-4212' } });
|
||||||
|
const r = await probeInPlaceUpdate(sftp as never, PREFIX, 'srvmid_db');
|
||||||
|
expect(r.ok).toBe(false);
|
||||||
|
expect(r.reason).toMatch(/restauración en curso/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('el motivo del rechazo nunca queda vacío: cada caso tiene su remedio', async () => {
|
||||||
|
const casos = [
|
||||||
|
{ ...OK, 'test -w': { code: 1, stdout: 'no' } },
|
||||||
|
{ ...OK, 'systemctl show': { code: 0, stdout: 'User=otro\nRestart=always' } },
|
||||||
|
{ ...OK, 'systemctl show': { code: 0, stdout: 'User=srvmid_db\nRestart=no' } },
|
||||||
|
{ ...OK, '/Temp': { code: 0, stdout: 'job-1' } }
|
||||||
|
];
|
||||||
|
for (const caso of casos) {
|
||||||
|
const { sftp } = sftpFor(caso);
|
||||||
|
const r = await probeInPlaceUpdate(sftp as never, PREFIX, 'srvmid_db');
|
||||||
|
expect(r.ok).toBe(false);
|
||||||
|
expect(r.reason.trim().length).toBeGreaterThan(20);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -504,21 +504,37 @@ async function installLinux(
|
|||||||
// antes de transferir 270 MB, porque fallar después es desperdicio y deja basura en /tmp.
|
// antes de transferir 270 MB, porque fallar después es desperdicio y deja basura en /tmp.
|
||||||
const autostartMode = request.autostart ?? 'service';
|
const autostartMode = request.autostart ?? 'service';
|
||||||
const privileged = await probeLinuxElevation(sftp);
|
const privileged = await probeLinuxElevation(sftp);
|
||||||
if (autostartMode !== 'user-service' && !privileged.prefix && privileged.elevation !== 'root') {
|
const canElevate = privileged.elevation === 'root' || privileged.elevation === 'sudo-sin-password';
|
||||||
|
|
||||||
|
// Sin elevación quedan dos vías antes de rendirse, y ambas evitan pedirle la contraseña a
|
||||||
|
// sudo. `user-service` instala en el home; la actualización en sitio deja el unit como está
|
||||||
|
// y solo reemplaza el binario, que es lo único que una actualización necesita de verdad.
|
||||||
|
let inPlaceUpdate = false;
|
||||||
|
if (!canElevate && autostartMode !== 'user-service') {
|
||||||
|
if (request.mode !== 'update') {
|
||||||
throw new InstallError(
|
throw new InstallError(
|
||||||
409,
|
409,
|
||||||
`El usuario '${target.ssh_username}' no puede elevar privilegios en este servidor: ` +
|
`El usuario '${target.ssh_username}' no puede elevar privilegios: ` +
|
||||||
`${privileged.detail} Para instalar en ${installPath} y registrar el servicio de ` +
|
`${privileged.detail} Una instalación NUEVA en ${installPath} necesita ` +
|
||||||
'sistema hacen falta. Alternativas: usar una cuenta root, dar NOPASSWD a ese ' +
|
'privilegios para registrar el servicio de sistema. Usa el arranque ' +
|
||||||
'usuario, o elegir el arranque "Servicio de usuario", que instala en el home y no ' +
|
'"Servicio de usuario", que instala en el home y no los necesita.'
|
||||||
'necesita privilegios.'
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const inPlace = await probeInPlaceUpdate(sftp, installPath, target.ssh_username);
|
||||||
|
if (!inPlace.ok) {
|
||||||
|
throw new InstallError(
|
||||||
|
409,
|
||||||
|
`No se puede actualizar ${target.name} sin privilegios: ${inPlace.reason}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
inPlaceUpdate = true;
|
||||||
|
}
|
||||||
await appendInstallStep(
|
await appendInstallStep(
|
||||||
runId,
|
runId,
|
||||||
'precondiciones',
|
'precondiciones',
|
||||||
true,
|
true,
|
||||||
`${systemEvidence}, privilegios=${privileged.label}`
|
`${systemEvidence}, privilegios=${privileged.label}` +
|
||||||
|
(inPlaceUpdate ? ', vía=actualización en sitio sin privilegios' : '')
|
||||||
);
|
);
|
||||||
|
|
||||||
// Antes de transferir: si es una actualización, confirmar que ahí VIVE una instalación.
|
// Antes de transferir: si es una actualización, confirmar que ahí VIVE una instalación.
|
||||||
@@ -591,7 +607,13 @@ async function installLinux(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const autostart = request.autostart ?? 'service';
|
const autostart = request.autostart ?? 'service';
|
||||||
const installerFlags = autostart === 'none' ? '' : ` --${autostart}`;
|
// En la vía sin privilegios se usa --update-in-place, que no toca /etc ni systemctl:
|
||||||
|
// solo reemplaza el binario y señaliza al proceso para que Restart=always lo relevante.
|
||||||
|
const installerFlags = inPlaceUpdate
|
||||||
|
? ' --update-in-place'
|
||||||
|
: autostart === 'none'
|
||||||
|
? ''
|
||||||
|
: ` --${autostart}`;
|
||||||
// PREFIX es variable de entorno en install.sh, no un flag. La ruta no es secreta, así
|
// PREFIX es variable de entorno en install.sh, no un flag. La ruta no es secreta, así
|
||||||
// que pasarla por la línea de comandos está bien; el token sí va por archivo.
|
// que pasarla por la línea de comandos está bien; el token sí va por archivo.
|
||||||
const installCmd =
|
const installCmd =
|
||||||
@@ -601,6 +623,17 @@ async function installLinux(
|
|||||||
|
|
||||||
await appendInstallStep(runId, 'ejecutar-instalador', true, `install.sh${installerFlags}`);
|
await appendInstallStep(runId, 'ejecutar-instalador', true, `install.sh${installerFlags}`);
|
||||||
const install = await execRemote(sftp, installCmd, INSTALL_EXEC_TIMEOUT_MS);
|
const install = await execRemote(sftp, installCmd, INSTALL_EXEC_TIMEOUT_MS);
|
||||||
|
// 75 = EX_TEMPFAIL, que install.sh reserva para "hay una restauración en curso". Es un
|
||||||
|
// conflicto de estado, no un fallo: hay que reintentar, no investigar. Con el 502 genérico
|
||||||
|
// parecía que la instalación se había roto, y el operador iría a buscar la causa equivocada.
|
||||||
|
if (install.code === 75) {
|
||||||
|
throw new InstallError(
|
||||||
|
409,
|
||||||
|
`${target.name} está restaurando un respaldo ahora mismo, así que no se actualizó ` +
|
||||||
|
'para no interrumpirlo —eso dejaría ese respaldo vetado y la base en ' +
|
||||||
|
'SINGLE_USER—. Reintenta cuando termine.'
|
||||||
|
);
|
||||||
|
}
|
||||||
if (install.code !== 0) {
|
if (install.code !== 0) {
|
||||||
throw new InstallError(
|
throw new InstallError(
|
||||||
502,
|
502,
|
||||||
@@ -609,7 +642,9 @@ async function installLinux(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Verificación del despliegue -------------------------------------
|
// --- Verificación del despliegue -------------------------------------
|
||||||
await verifyLinuxDeployment(sftp, runId, release, privileged, autostart, installPath);
|
await verifyLinuxDeployment(
|
||||||
|
sftp, runId, release, privileged, autostart, installPath, inPlaceUpdate
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
// Limpieza siempre: el panel.env trae el token en claro. Si falla NO se silencia —
|
// Limpieza siempre: el panel.env trae el token en claro. Si falla NO se silencia —
|
||||||
// significa que el token se quedó en el servidor y alguien tiene que ir a borrarlo.
|
// significa que el token se quedó en el servidor y alguien tiene que ir a borrarlo.
|
||||||
@@ -696,6 +731,87 @@ export async function probeLinuxElevation(sftp: SftpClient): Promise<LinuxPrivil
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ¿Se puede ACTUALIZAR en su sitio sin privilegios? Devuelve el motivo si no.
|
||||||
|
*
|
||||||
|
* Existe porque hay entornos donde no se usa root en absoluto. Una actualización con el unit ya
|
||||||
|
* registrado no necesita privilegios si se cumplen tres cosas, y las tres se comprueban por SSH
|
||||||
|
* antes de subir 270 MB:
|
||||||
|
*
|
||||||
|
* 1. `install` puede reemplazar el binario. Y no exige permiso sobre el ARCHIVO: a diferencia de
|
||||||
|
* `cp`, desvincula el destino antes de crearlo (por eso `make install` funciona sobre binarios
|
||||||
|
* en ejecución), así que lo que hace falta es escritura en el DIRECTORIO.
|
||||||
|
* 2. El unit corre con el mismo usuario de la sesión SSH, para poder señalizar al proceso. Si
|
||||||
|
* quedó con `User=root` —porque alguien instaló desde un `sudo -i`— esta vía no aplica.
|
||||||
|
* 3. El unit trae `Restart=always`, que es quien vuelve a levantarlo con el binario nuevo. Sin
|
||||||
|
* eso, señalizarlo lo dejaría muerto.
|
||||||
|
*/
|
||||||
|
export async function probeInPlaceUpdate(
|
||||||
|
sftp: SftpClient,
|
||||||
|
installPath: string,
|
||||||
|
sshUsername: string
|
||||||
|
): Promise<{ ok: boolean; reason: string }> {
|
||||||
|
const writable = await execRemote(
|
||||||
|
sftp,
|
||||||
|
`test -w ${shQuote(installPath)} && echo si || echo no`
|
||||||
|
);
|
||||||
|
if (writable.stdout.trim() !== 'si') {
|
||||||
|
const owner = await execRemote(sftp, `stat -c '%U' ${shQuote(installPath)} 2>/dev/null`);
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason:
|
||||||
|
`'${sshUsername}' no puede escribir en ${installPath} (dueño: ` +
|
||||||
|
`${owner.stdout.trim() || 'desconocido'}), así que no puede reemplazar el binario.`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// `systemctl show` se lee sin privilegios, a diferencia de start/stop.
|
||||||
|
const unit = await execRemote(
|
||||||
|
sftp,
|
||||||
|
'systemctl show -p User -p Restart cloudrestoreas 2>/dev/null'
|
||||||
|
);
|
||||||
|
const props = new Map(
|
||||||
|
unit.stdout
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => line.trim().split('='))
|
||||||
|
.filter((parts) => parts.length === 2)
|
||||||
|
.map(([k, v]) => [k, v] as const)
|
||||||
|
);
|
||||||
|
// User vacío en systemd significa root: el unit no declaró User=.
|
||||||
|
const unitUser = props.get('User') || 'root';
|
||||||
|
if (unitUser !== sshUsername) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason:
|
||||||
|
`el servicio corre como '${unitUser}' y la sesión es de '${sshUsername}', así que ` +
|
||||||
|
'esta cuenta no puede reiniciarlo sin privilegios.'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (props.get('Restart') !== 'always') {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason:
|
||||||
|
`el unit tiene Restart=${props.get('Restart') || 'desconocido'}; sin Restart=always ` +
|
||||||
|
'nadie volvería a levantar el agente tras actualizarlo.'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se mira aquí además de en install.sh para no subir 270 MB en balde.
|
||||||
|
const busy = await execRemote(
|
||||||
|
sftp,
|
||||||
|
`test -d ${shQuote(`${installPath}/Temp`)} && ls -A ${shQuote(`${installPath}/Temp`)} 2>/dev/null | head -1`
|
||||||
|
);
|
||||||
|
if (busy.stdout.trim()) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason:
|
||||||
|
'hay una restauración en curso. Interrumpirla dejaría ese respaldo vetado para ' +
|
||||||
|
'siempre y la base en SINGLE_USER, así que se prefiere esperar. Reintenta luego.'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { ok: true, reason: '' };
|
||||||
|
}
|
||||||
|
|
||||||
async function verifyLinuxDeployment(
|
async function verifyLinuxDeployment(
|
||||||
sftp: SftpClient,
|
sftp: SftpClient,
|
||||||
runId: number,
|
runId: number,
|
||||||
@@ -705,19 +821,36 @@ async function verifyLinuxDeployment(
|
|||||||
// Se llama installPath y no `prefix` a propósito: en el cuerpo convive con
|
// Se llama installPath y no `prefix` a propósito: en el cuerpo convive con
|
||||||
// `privileged.prefix`, que es el prefijo de ELEVACIÓN. Dos cosas distintas con el mismo
|
// `privileged.prefix`, que es el prefijo de ELEVACIÓN. Dos cosas distintas con el mismo
|
||||||
// nombre en el mismo alcance es exactamente donde se cuela un bug silencioso.
|
// nombre en el mismo alcance es exactamente donde se cuela un bug silencioso.
|
||||||
installPath: string
|
installPath: string,
|
||||||
|
inPlaceUpdate = false
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// El sello config/.version lo escribe el bootstrap del binario; es más confiable que
|
// El sello config/.version lo escribe el bootstrap del binario; es más confiable que
|
||||||
// stdout de --version, sobre todo por paridad con Windows (console=False).
|
// stdout de --version, sobre todo por paridad con Windows (console=False).
|
||||||
|
//
|
||||||
|
// En la vía en sitio se OMITE el bootstrap y el sello lo escribe el proceso al reiniciarse,
|
||||||
|
// así que durante unos segundos sigue teniendo la versión ANTERIOR. Leerlo una sola vez
|
||||||
|
// convertía una actualización correcta en un fallo "reporta 1.0.0, se esperaba 1.1.1", con el
|
||||||
|
// agente ya actualizado y corriendo. Se sondea hasta que coincida, o hasta agotar el margen.
|
||||||
|
let deployed = '';
|
||||||
|
const stampAttempts = inPlaceUpdate ? 15 : 1;
|
||||||
|
for (let attempt = 0; attempt < stampAttempts; attempt += 1) {
|
||||||
|
if (attempt > 0) await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
const stamp = await execRemote(
|
const stamp = await execRemote(
|
||||||
sftp,
|
sftp,
|
||||||
`cat ${shQuote(`${installPath}/config/.version`)} 2>/dev/null`
|
`cat ${shQuote(`${installPath}/config/.version`)} 2>/dev/null`
|
||||||
);
|
);
|
||||||
const deployed = stamp.stdout.trim();
|
deployed = stamp.stdout.trim();
|
||||||
|
// Vacío = todavía no lo escribió, y eso ya se toleraba antes.
|
||||||
|
if (!deployed || deployed === release.version) break;
|
||||||
|
}
|
||||||
if (deployed && deployed !== release.version) {
|
if (deployed && deployed !== release.version) {
|
||||||
throw new InstallError(
|
throw new InstallError(
|
||||||
502,
|
502,
|
||||||
`El binario desplegado reporta la versión ${deployed}, se esperaba ${release.version}`
|
`El binario desplegado reporta la versión ${deployed}, se esperaba ${release.version}. ` +
|
||||||
|
(inPlaceUpdate
|
||||||
|
? 'El sello no cambió tras reiniciar el agente, así que el proceso puede seguir ' +
|
||||||
|
'ejecutando el binario anterior.'
|
||||||
|
: '')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
await appendInstallStep(
|
await appendInstallStep(
|
||||||
@@ -733,6 +866,9 @@ async function verifyLinuxDeployment(
|
|||||||
// usuario, y preguntarlo como root apuntaría al bus equivocado y respondería 'inactive'
|
// usuario, y preguntarlo como root apuntaría al bus equivocado y respondería 'inactive'
|
||||||
// sobre un servicio que sí está corriendo. XDG_RUNTIME_DIR va explícito porque un `exec` de
|
// sobre un servicio que sí está corriendo. XDG_RUNTIME_DIR va explícito porque un `exec` de
|
||||||
// SSH no es una sesión de login y no siempre lo trae.
|
// SSH no es una sesión de login y no siempre lo trae.
|
||||||
|
// En la vía en sitio el agente acaba de reiniciarse: `is-active` puede devolver 'activating'
|
||||||
|
// durante unos segundos, y eso no es un fallo. install.sh ya confirmó que el proceso volvió,
|
||||||
|
// así que el proceso vivo es la evidencia que manda.
|
||||||
const userMode = autostart === 'user-service';
|
const userMode = autostart === 'user-service';
|
||||||
const active = await execRemote(
|
const active = await execRemote(
|
||||||
sftp,
|
sftp,
|
||||||
@@ -745,7 +881,7 @@ async function verifyLinuxDeployment(
|
|||||||
// En modo usuario el arranque puede haber quedado por cron (@reboot) en vez de systemd,
|
// En modo usuario el arranque puede haber quedado por cron (@reboot) en vez de systemd,
|
||||||
// cuando el destino no permite lingering. Ahí no hay unit que consultar y el proceso se
|
// cuando el destino no permite lingering. Ahí no hay unit que consultar y el proceso se
|
||||||
// comprueba directamente, que es lo que de verdad importa.
|
// comprueba directamente, que es lo que de verdad importa.
|
||||||
if (userMode) {
|
if (userMode || inPlaceUpdate) {
|
||||||
// El ancla ^ es obligatoria: sin ella el `sh -c` que corre este mismo pgrep lleva la
|
// El ancla ^ es obligatoria: sin ella el `sh -c` que corre este mismo pgrep lleva la
|
||||||
// ruta en su propia línea de comandos y haría match consigo mismo, reportando vivo un
|
// ruta en su propia línea de comandos y haría match consigo mismo, reportando vivo un
|
||||||
// agente que no arrancó.
|
// agente que no arrancó.
|
||||||
@@ -758,7 +894,9 @@ async function verifyLinuxDeployment(
|
|||||||
runId,
|
runId,
|
||||||
'verificar-servicio',
|
'verificar-servicio',
|
||||||
true,
|
true,
|
||||||
'proceso vivo (arranque por cron; systemd de usuario no disponible)'
|
inPlaceUpdate
|
||||||
|
? `proceso vivo con el binario nuevo (unit intacto, estado: ${state})`
|
||||||
|
: 'proceso vivo (arranque por cron; systemd de usuario no disponible)'
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user