fix(client): v1.3.0 — anti-loop updater, config backup, NSIS safe
Itens 0, 3 e 4 do plano de correção:
ITEM 0 — Preservar dados na atualização (não reconfigurar do zero)
- index.js: loadConfig/saveConfig grava config.backup.json a cada
gravação válida; ao iniciar, restaura do backup se config.json
sumir ou corromper — protege clientId e credenciais.
- client-monitor.js: nova persistConfig() grava config + backup juntos;
aplicada nos 3 pontos que escrevem o config (clientId init, painel
local, wizard CLI).
- package.json: deleteAppDataOnUninstall: false explícito.
ITEM 3 — Duplicatas na lista de apps do Windows
- Mantido perMachine: false consistente; GUID derivado do appId
(não adicionado explícito para não criar 3ª entrada).
- deleteAppDataOnUninstall: false garante AppData intacto.
ITEM 4 — Loop de notificação de nova versão
- Trava anti-loop: conta tentativas por versão em update-state.json;
após 3 falhas da mesma versão, para de auto-instalar e notifica
balão com mensagem para reinstalar manualmente.
- reconcileUpdateState() no boot zera o contador quando a versão
realmente avançou (instalação bem-sucedida).
- autoUpdater.on('error') agora exibe balão com erro real (throttle 60s).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -66,6 +66,55 @@ autoUpdater.autoDownload = true;
|
||||
autoUpdater.autoInstallOnAppQuit = true;
|
||||
autoUpdater.logger = require('electron').app ? console : null;
|
||||
|
||||
// ---- Trava anti-loop de atualização --------------------------------
|
||||
// Se uma instalação falha (ex.: install incompleto), o app reabre na MESMA
|
||||
// versão, detecta o update de novo e reentra em loop. Aqui contamos quantas
|
||||
// vezes tentamos instalar cada versão; após o limite, paramos de auto-instalar
|
||||
// e avisamos o usuário para reinstalar manualmente — quebrando o loop.
|
||||
const MAX_UPDATE_ATTEMPTS = 3;
|
||||
const updateStatePath = () => path.join(app.getPath('userData'), 'update-state.json');
|
||||
|
||||
function readUpdateState() {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(updateStatePath(), 'utf8')) || {};
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function writeUpdateState(state) {
|
||||
try {
|
||||
fs.writeFileSync(updateStatePath(), JSON.stringify(state, null, 2), 'utf8');
|
||||
} catch (e) {
|
||||
console.error('Erro ao gravar update-state.json:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Chamado no boot: se a versão atual já alcançou (ou passou) a versão tentada,
|
||||
// a instalação teve sucesso → zera o contador.
|
||||
function reconcileUpdateState() {
|
||||
const state = readUpdateState();
|
||||
if (state.version && state.version === app.getVersion()) {
|
||||
console.log(`✅ Atualização para v${state.version} concluída — limpando contador.`);
|
||||
writeUpdateState({});
|
||||
}
|
||||
}
|
||||
|
||||
let lastUpdaterErrorAt = 0;
|
||||
function notifyUpdaterError(message) {
|
||||
const now = Date.now();
|
||||
// Throttle: no máximo 1 balão a cada 60s para não inundar o usuário
|
||||
if (now - lastUpdaterErrorAt < 60000) return;
|
||||
lastUpdaterErrorAt = now;
|
||||
if (tray) {
|
||||
tray.displayBalloon({
|
||||
title: 'ScoreOdonto - Falha na Atualização',
|
||||
content: message,
|
||||
iconType: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
autoUpdater.on('checking-for-update', () => {
|
||||
console.log('🔍 Verificando atualizações...');
|
||||
});
|
||||
@@ -100,7 +149,28 @@ autoUpdater.on('download-progress', (progress) => {
|
||||
});
|
||||
|
||||
autoUpdater.on('update-downloaded', (info) => {
|
||||
console.log('✅ Atualização baixada:', info.version, '— reiniciando em 15 segundos...');
|
||||
console.log('✅ Atualização baixada:', info.version);
|
||||
|
||||
// --- Trava anti-loop: não reinstalar a mesma versão indefinidamente ---
|
||||
const state = readUpdateState();
|
||||
const attempts = (state.version === info.version ? state.attempts : 0) || 0;
|
||||
|
||||
if (attempts >= MAX_UPDATE_ATTEMPTS) {
|
||||
console.warn(`🛑 v${info.version} já falhou ${attempts}x — abortando auto-instalação para evitar loop.`);
|
||||
downloadProgress = null;
|
||||
updateTrayMenu();
|
||||
notifyUpdaterError(
|
||||
`A atualização para a versão ${info.version} falhou ${attempts} vezes e foi interrompida. ` +
|
||||
`Por favor, baixe e instale o cliente manualmente para concluir.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Registra mais uma tentativa ANTES de reiniciar; será zerado no próximo
|
||||
// boot se a versão tiver realmente avançado (reconcileUpdateState).
|
||||
writeUpdateState({ version: info.version, attempts: attempts + 1 });
|
||||
|
||||
console.log(`↻ Reiniciando em 15s para instalar v${info.version} (tentativa ${attempts + 1}/${MAX_UPDATE_ATTEMPTS})...`);
|
||||
downloadProgress = "Pronto";
|
||||
|
||||
if (tray) {
|
||||
@@ -131,10 +201,12 @@ autoUpdater.on('error', (err) => {
|
||||
console.error('❌ Erro no auto-updater:', err.message);
|
||||
downloadProgress = null;
|
||||
updateTrayMenu();
|
||||
notifyUpdaterError(`Não foi possível atualizar: ${err.message}`);
|
||||
});
|
||||
|
||||
const appDataFolder = app.getPath('userData');
|
||||
const configPath = path.join(appDataFolder, 'config.json');
|
||||
const configBackupPath = path.join(appDataFolder, 'config.backup.json');
|
||||
|
||||
// Ensure directory exists
|
||||
if (!fs.existsSync(appDataFolder)) {
|
||||
@@ -155,24 +227,71 @@ const defaultConfig = {
|
||||
adminPassword: "admin"
|
||||
};
|
||||
|
||||
function loadConfig() {
|
||||
// Lê e valida um JSON de config; retorna null se ausente/corrompido
|
||||
function readConfigFile(filePath) {
|
||||
try {
|
||||
if (!fs.existsSync(configPath)) {
|
||||
fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2), 'utf8');
|
||||
return defaultConfig;
|
||||
if (!fs.existsSync(filePath)) return null;
|
||||
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
// Só consideramos válido se for um objeto com ao menos um campo
|
||||
if (parsed && typeof parsed === 'object' && Object.keys(parsed).length > 0) {
|
||||
return parsed;
|
||||
}
|
||||
const userConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
// Ensure required fields exist
|
||||
return { ...defaultConfig, ...userConfig };
|
||||
return null;
|
||||
} catch (e) {
|
||||
console.error("Error loading config:", e);
|
||||
console.error(`Error reading config file ${filePath}:`, e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Mantém uma cópia de segurança do config para não perder clientId/credenciais
|
||||
// em caso de corrupção (evita reconfiguração do cliente do zero).
|
||||
function writeConfigBackup(cfg) {
|
||||
try {
|
||||
fs.writeFileSync(configBackupPath, JSON.stringify(cfg, null, 2), 'utf8');
|
||||
} catch (e) {
|
||||
console.error("Error writing config backup:", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
// 1. Tenta o arquivo principal
|
||||
let userConfig = readConfigFile(configPath);
|
||||
|
||||
// 2. Se falhar, tenta restaurar do backup (rede de segurança)
|
||||
if (!userConfig) {
|
||||
const backup = readConfigFile(configBackupPath);
|
||||
if (backup) {
|
||||
console.warn("⚠️ config.json ausente/corrompido — restaurando a partir de config.backup.json");
|
||||
userConfig = backup;
|
||||
try {
|
||||
fs.writeFileSync(configPath, JSON.stringify(backup, null, 2), 'utf8');
|
||||
} catch (e) {
|
||||
console.error("Falha ao restaurar config.json do backup:", e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Sem principal nem backup: primeira execução → grava template padrão
|
||||
if (!userConfig) {
|
||||
try {
|
||||
fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2), 'utf8');
|
||||
} catch (e) {
|
||||
console.error("Error writing default config:", e.message);
|
||||
}
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
const merged = { ...defaultConfig, ...userConfig };
|
||||
// Mantém o backup atualizado sempre que carregamos um config válido
|
||||
writeConfigBackup(merged);
|
||||
return merged;
|
||||
}
|
||||
|
||||
function saveConfig(newConfig) {
|
||||
try {
|
||||
fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2), 'utf8');
|
||||
// Atualiza o backup logo após gravar com sucesso
|
||||
writeConfigBackup(newConfig);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error("Error saving config:", e);
|
||||
@@ -401,6 +520,10 @@ app.on('second-instance', () => {
|
||||
app.whenReady().then(() => {
|
||||
initTray();
|
||||
|
||||
// Se a versão atual já é a que tentávamos instalar, a atualização teve
|
||||
// sucesso → zera o contador anti-loop antes de checar novamente.
|
||||
reconcileUpdateState();
|
||||
|
||||
// Verificar atualizações silenciosamente ao iniciar
|
||||
autoUpdater.checkForUpdatesAndNotify();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user