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:
+37
-6
@@ -41,11 +41,42 @@ if (!fs.existsSync(appDataFolder)) {
|
|||||||
|
|
||||||
// Carregar configuração dinamicamente para poder atualizá-la
|
// Carregar configuração dinamicamente para poder atualizá-la
|
||||||
const configPath = path.join(appDataFolder, 'config.json');
|
const configPath = path.join(appDataFolder, 'config.json');
|
||||||
|
const configBackupPath = path.join(appDataFolder, 'config.backup.json');
|
||||||
|
|
||||||
|
// Lê e valida um JSON de config; retorna null se ausente/corrompido
|
||||||
|
function readConfigFileSafe(filePath) {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(filePath)) return null;
|
||||||
|
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||||
|
if (parsed && typeof parsed === 'object' && Object.keys(parsed).length > 0) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grava config.json + cópia de segurança (preserva clientId/credenciais)
|
||||||
|
function persistConfig() {
|
||||||
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(configBackupPath, JSON.stringify(config, null, 2));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Erro ao gravar backup de config:', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let config = {};
|
let config = {};
|
||||||
let configChanged = false;
|
let configChanged = false;
|
||||||
try {
|
// Tenta principal e, se faltar/corromper, restaura do backup antes de cair no default
|
||||||
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
config = readConfigFileSafe(configPath) || readConfigFileSafe(configBackupPath);
|
||||||
} catch (e) {
|
if (config) {
|
||||||
|
if (!fs.existsSync(configPath)) {
|
||||||
|
// Restaurado do backup: regrava o principal
|
||||||
|
configChanged = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
config = {
|
config = {
|
||||||
serverUrl: "https://rx.scoreodonto.com",
|
serverUrl: "https://rx.scoreodonto.com",
|
||||||
monitorPath: "C:\\ProgramData\\RF\\Dental Sensor\\Images",
|
monitorPath: "C:\\ProgramData\\RF\\Dental Sensor\\Images",
|
||||||
@@ -67,7 +98,7 @@ if (!config.clientId) {
|
|||||||
|
|
||||||
if (configChanged) {
|
if (configChanged) {
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
persistConfig();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Erro ao inicializar clientId no config.json:', err.message);
|
console.error('Erro ao inicializar clientId no config.json:', err.message);
|
||||||
}
|
}
|
||||||
@@ -858,7 +889,7 @@ function startAdminServer() {
|
|||||||
config.adminPassword = newConfig.adminPassword;
|
config.adminPassword = newConfig.adminPassword;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
persistConfig();
|
||||||
addLog(`💾 Novas configurações persistidas em config.json`);
|
addLog(`💾 Novas configurações persistidas em config.json`);
|
||||||
} catch (writeErr) {
|
} catch (writeErr) {
|
||||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||||
@@ -1493,7 +1524,7 @@ function runSetupWizard() {
|
|||||||
config.clientName = chosenName;
|
config.clientName = chosenName;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
persistConfig();
|
||||||
console.log('═══════════════════════════════════════════════════════');
|
console.log('═══════════════════════════════════════════════════════');
|
||||||
console.log('✅ CONFIGURAÇÃO SALVA COM SUCESSO EM config.json!');
|
console.log('✅ CONFIGURAÇÃO SALVA COM SUCESSO EM config.json!');
|
||||||
console.log(` Servidor Destino: ${config.serverUrl}`);
|
console.log(` Servidor Destino: ${config.serverUrl}`);
|
||||||
|
|||||||
@@ -66,6 +66,55 @@ autoUpdater.autoDownload = true;
|
|||||||
autoUpdater.autoInstallOnAppQuit = true;
|
autoUpdater.autoInstallOnAppQuit = true;
|
||||||
autoUpdater.logger = require('electron').app ? console : null;
|
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', () => {
|
autoUpdater.on('checking-for-update', () => {
|
||||||
console.log('🔍 Verificando atualizações...');
|
console.log('🔍 Verificando atualizações...');
|
||||||
});
|
});
|
||||||
@@ -100,7 +149,28 @@ autoUpdater.on('download-progress', (progress) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
autoUpdater.on('update-downloaded', (info) => {
|
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";
|
downloadProgress = "Pronto";
|
||||||
|
|
||||||
if (tray) {
|
if (tray) {
|
||||||
@@ -131,10 +201,12 @@ autoUpdater.on('error', (err) => {
|
|||||||
console.error('❌ Erro no auto-updater:', err.message);
|
console.error('❌ Erro no auto-updater:', err.message);
|
||||||
downloadProgress = null;
|
downloadProgress = null;
|
||||||
updateTrayMenu();
|
updateTrayMenu();
|
||||||
|
notifyUpdaterError(`Não foi possível atualizar: ${err.message}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
const appDataFolder = app.getPath('userData');
|
const appDataFolder = app.getPath('userData');
|
||||||
const configPath = path.join(appDataFolder, 'config.json');
|
const configPath = path.join(appDataFolder, 'config.json');
|
||||||
|
const configBackupPath = path.join(appDataFolder, 'config.backup.json');
|
||||||
|
|
||||||
// Ensure directory exists
|
// Ensure directory exists
|
||||||
if (!fs.existsSync(appDataFolder)) {
|
if (!fs.existsSync(appDataFolder)) {
|
||||||
@@ -155,24 +227,71 @@ const defaultConfig = {
|
|||||||
adminPassword: "admin"
|
adminPassword: "admin"
|
||||||
};
|
};
|
||||||
|
|
||||||
function loadConfig() {
|
// Lê e valida um JSON de config; retorna null se ausente/corrompido
|
||||||
|
function readConfigFile(filePath) {
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(configPath)) {
|
if (!fs.existsSync(filePath)) return null;
|
||||||
fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2), 'utf8');
|
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||||
return defaultConfig;
|
// 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'));
|
return null;
|
||||||
// Ensure required fields exist
|
|
||||||
return { ...defaultConfig, ...userConfig };
|
|
||||||
} catch (e) {
|
} 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;
|
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) {
|
function saveConfig(newConfig) {
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2), 'utf8');
|
fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2), 'utf8');
|
||||||
|
// Atualiza o backup logo após gravar com sucesso
|
||||||
|
writeConfigBackup(newConfig);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error saving config:", e);
|
console.error("Error saving config:", e);
|
||||||
@@ -401,6 +520,10 @@ app.on('second-instance', () => {
|
|||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
initTray();
|
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
|
// Verificar atualizações silenciosamente ao iniciar
|
||||||
autoUpdater.checkForUpdatesAndNotify();
|
autoUpdater.checkForUpdatesAndNotify();
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "rx.scoreodonto.com-client",
|
"name": "rx.scoreodonto.com-client",
|
||||||
"version": "1.2.9",
|
"version": "1.3.0",
|
||||||
"author": "ScoreOdonto",
|
"author": "ScoreOdonto",
|
||||||
"description": "Cliente Windows para envio de imagens dentais da ScoreOdonto",
|
"description": "Cliente Windows para envio de imagens dentais da ScoreOdonto",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
"nsis": {
|
"nsis": {
|
||||||
"oneClick": true,
|
"oneClick": true,
|
||||||
"perMachine": false,
|
"perMachine": false,
|
||||||
|
"deleteAppDataOnUninstall": false,
|
||||||
"allowToChangeInstallationDirectory": false,
|
"allowToChangeInstallationDirectory": false,
|
||||||
"createDesktopShortcut": true,
|
"createDesktopShortcut": true,
|
||||||
"createStartMenuShortcut": true,
|
"createStartMenuShortcut": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user