feat(update): one-click desktop self-update (AppImage, .deb, Windows)
update_apply used to flatly refuse to run for the GUI binary and point
the user to a manual download instead. Since both desktop apps already
elevate to root/administrator before showing their window, the
process applying the update already has everything it needs — no
second authentication round is required.
- New guara_core::update::apply_desktop[_with_progress], alongside the
existing CLI-only apply() (left untouched): detects the install kind
at runtime (AppImage via the APPIMAGE env var — current_exe() is
unreliable there, same FUSE-mount issue elevated_exe() already works
around and this reuses; Deb otherwise on Linux; WindowsInstaller on
Windows), matches the right release asset by the GUI bundle naming
(_amd64/_aarch64.AppImage, _amd64/_arm64.deb, _x64-setup.exe — a
different suffix convention than the CLI tarballs), verifies SHA-256
when a sidecar is published, and installs:
- AppImage: downloads to a temp file in the same directory (same
filesystem, atomic rename), renames over the running file, execs
it in place (same PID, same already-root privileges — no elevation
dance needed).
- .deb: runs `dpkg -i` directly (already root), then execs
current_exe() again now that dpkg replaced it on disk.
- Windows: runs the downloaded installer with /S (silent) detached,
then exits immediately to release the file lock on our own .exe.
Does NOT claim to relaunch the app afterwards — there is no
documented NSIS or Tauri flag for that (verified against NSIS's
and Tauri's own docs: Tauri's own updater restarts the app from its
own Rust code after the installer exits, not via a command-line
switch), and blocking here on the installer's exit status risks a
self-deadlock if its script waits for this process to close before
it can overwrite the running .exe. Needs a real Windows machine to
confirm the actual post-install behavior.
- Engine::update_apply_desktop wires it into the Tauri `update_apply`
command, which now also takes AppHandle and emits `update-progress`
events (downloading/verifying/installing/restarting) for the UI.
- Sabiá: the existing #btnUpdate handler changed the button's label to
"update to X" but never actually called apply — wired it for real,
with the same progress feedback.
- Guará: restored the "Update now" action in the update card (removed
during the updater-unification migration), wired to the new command.
- scripts/build-gui.sh: generates a .sha256 next to every file in
dist-gui/, matching the existing convention for CLI release
archives, so apply_desktop can verify downloads the same way CLI
update --apply already does.
- 3 new unit tests per repo: install-kind detection, GUI asset suffix
matching against real release naming, and the existing CLI-refusal
test still passes unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VAyr8d7HuAdUGB6aA5xmDD
This commit is contained in:
1 parent
21eb4eafae
commit
cdda2bc45f
6 files changed
+424
-7
No files matched your search
+46
-2
@@ -750,7 +750,11 @@ $('#settingsBtn')?.addEventListener('click', () => {
|
||||
${tauri?.core?.invoke ? `<label class="check"><input type="checkbox" name="updates" ${SETTINGS.updates ? 'checked' : ''}><span><b>Atualizações automáticas</b><br><span class="sub">Ao abrir, consulta o Gitea para ver se há versão nova (uma consulta, sem enviar dados seus)</span><br><button type="button" class="btn sm" id="updNow">Verificar agora</button></span></label>` : ''}
|
||||
<menu><button class="btn" value="cancel">Cancelar</button><button class="btn primary" value="ok">Salvar</button></menu></form>`;
|
||||
d.querySelector('[name=lang]').value = SETTINGS.lang;
|
||||
d.querySelector('#updNow')?.addEventListener('click', (e) => { e.preventDefault(); checkUpdate(true); });
|
||||
d.querySelector('#updNow')?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
if (UPD) applyUpdate();
|
||||
else checkUpdate(true);
|
||||
});
|
||||
d.onclose = () => {
|
||||
if (d.returnValue !== 'ok') return;
|
||||
const patch = { lang: d.querySelector('[name=lang]').value, notify: d.querySelector('[name=notify]').checked, maps: d.querySelector('[name=maps]').checked };
|
||||
@@ -768,6 +772,44 @@ $('#settingsBtn')?.addEventListener('click', () => {
|
||||
// The desktop shell (Tauri) queries Gitea Releases API and notifies the user.
|
||||
// No-op in browser mode (`guara ui`).
|
||||
let UPD = null;
|
||||
let updating = false;
|
||||
|
||||
async function applyUpdate() {
|
||||
if (updating || !tauri?.core?.invoke) return;
|
||||
updating = true;
|
||||
const updApply = $('#updApply');
|
||||
const updNow = $('#updNow');
|
||||
const setPhase = (txt) => {
|
||||
if (updApply) { updApply.disabled = true; updApply.textContent = txt; }
|
||||
if (updNow) { updNow.disabled = true; updNow.textContent = txt; }
|
||||
};
|
||||
setPhase('Baixando a atualização…');
|
||||
|
||||
let unlisten = null;
|
||||
if (tauri?.event?.listen) {
|
||||
try {
|
||||
unlisten = await tauri.event.listen('update-progress', (event) => {
|
||||
const phase = event.payload?.phase;
|
||||
if (phase === 'downloading') setPhase('Baixando a atualização…');
|
||||
else if (phase === 'verifying') setPhase('Verificando integridade…');
|
||||
else if (phase === 'installing') setPhase('Instalando a atualização…');
|
||||
else if (phase === 'restarting') setPhase('Reiniciando…');
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
try {
|
||||
await tauri.core.invoke('update_apply');
|
||||
setPhase('Atualização concluída');
|
||||
} catch (e) {
|
||||
updating = false;
|
||||
if (updApply) { updApply.disabled = false; updApply.textContent = 'Atualizar agora'; }
|
||||
if (updNow) { updNow.disabled = false; updNow.textContent = UPD?.version ? `atualizar para ${UPD.version}` : 'Verificar agora'; }
|
||||
toast(`Falha na atualização: ${e}`);
|
||||
} finally {
|
||||
if (unlisten) unlisten();
|
||||
}
|
||||
}
|
||||
|
||||
async function checkUpdate(manual) {
|
||||
if (!tauri?.core?.invoke) return;
|
||||
@@ -801,11 +843,13 @@ function renderUpdateCard() {
|
||||
if (!UPD) { el?.remove(); return; }
|
||||
if (!el) { el = document.createElement('div'); el.id = 'updateCard'; el.className = 'update-card'; el.setAttribute('role', 'status'); document.body.appendChild(el); }
|
||||
el.innerHTML = `<div class="upd-head"><b>Nova versão do Guará disponível</b><span class="mono upd-ver">v${esc(UPD.current)} → v${esc(UPD.version)}</span></div>
|
||||
<p class="sub">Esta instalação vem de um pacote do sistema e não se atualiza sozinha. Baixe a versão nova na página da release:</p><p class="mono upd-url">${esc(UPD.url)}</p>
|
||||
<p class="sub">Instalação automática da nova versão com um clique:</p>
|
||||
<div class="upd-actions">
|
||||
<button class="btn primary sm" id="updApply">Atualizar agora</button>
|
||||
<button class="btn sm" id="updLater">Depois</button>
|
||||
</div>`;
|
||||
$('#updLater')?.addEventListener('click', () => { UPD = null; el.remove(); });
|
||||
$('#updApply')?.addEventListener('click', () => applyUpdate());
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ close while scan is running
|
||||
|
||||
@@ -582,7 +582,7 @@ export const EN = {
|
||||
'Atualizando o Guará…': 'Updating Guará…',
|
||||
'Baixando a atualização…': 'Downloading the update…',
|
||||
'Baixando… {n}%': 'Downloading… {n}%',
|
||||
'Instalando… o Guará vai reabrir sozinho.': 'Installing… Guará will reopen by itself.',
|
||||
'Instalando a atualização…': 'Installing the update…',
|
||||
'Esta instalação vem de um pacote do sistema e não se atualiza sozinha. Baixe a versão nova na página da release:': 'This install comes from a system package and cannot update itself. Download the new version from the release page:',
|
||||
'Atualizações automáticas': 'Automatic updates',
|
||||
'Ao abrir, consulta o Gitea para ver se há versão nova (uma consulta, sem enviar dados seus)': 'On startup, checks Gitea for a new version (one request, nothing of yours is sent)',
|
||||
@@ -593,4 +593,9 @@ export const EN = {
|
||||
'Nova versão {n} disponível': 'New version {n} available',
|
||||
'Você já está na versão mais recente': 'You are already on the latest version',
|
||||
'Não foi possível verificar atualizações: {n}': 'Could not check for updates: {n}',
|
||||
'Instalação automática da nova versão com um clique:': 'One-click automatic installation of the new version:',
|
||||
'Verificando integridade…': 'Verifying checksum…',
|
||||
'Reiniciando…': 'Restarting…',
|
||||
'Atualização concluída': 'Update completed',
|
||||
'Falha na atualização: {n}': 'Update failed: {n}',
|
||||
};
|
||||
Reference in new issue
Block a user