fix(desktop): make remote backend updates terminal-state driven
Remote backend updates failed with "Backend update failed." on nearly every run: applyBackendUpdate() polled for only 30×1.5s ≈ 45s, then read exit_code null off the still-running action and called it a failure. Real updates (backup + uv sync + npm install + vite build) routinely run longer, and the no-op "Already up to date" path never restarted the gateway so the old return-check timed out too. A still-running, reachable action is now never converted into failure by an elapsed budget — only a nonzero exit is. The apply loop keeps one in-flight promise, tolerates reconnects during the dashboard restart without extending the fixed six-minute deadline forever, and confirms success by the action-specific receipt that survives the restart, falling back to proving the requested commit / up-to-date check for older backends without action_id support. Inconclusive completion fails closed. Fixes #47359 Fixes #58764 Co-authored-by: Vitor Cepeda Lopes <vitor@vitorcepedalopes.com> Co-authored-by: Mark Vlcek <markvlcek@gmail.com> Co-authored-by: doncazper <caztronics@yahoo.com>
This commit is contained in:
@@ -290,7 +290,11 @@ describe('requestActiveUpdate', () => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
afterEach(async () => {
|
||||
// Drain any backend apply this suite kicked off: applyBackendUpdate() now
|
||||
// memoizes the in-flight run, so a dangling promise here would be handed
|
||||
// to the next suite's tests instead of a fresh run.
|
||||
await vi.waitFor(() => expect($backendUpdateApply.get().applying).toBe(false), { timeout: 5000 })
|
||||
setRemote(false)
|
||||
delete (globalThis as unknown as { window?: unknown }).window
|
||||
})
|
||||
@@ -465,6 +469,7 @@ describe('applyBackendUpdate recovery', () => {
|
||||
checkHermesUpdateSpy.mockReset()
|
||||
updateHermesSpy.mockReset()
|
||||
getActionStatusSpy.mockReset()
|
||||
$backendUpdateStatus.set(null)
|
||||
$backendUpdateApply.set({
|
||||
applying: false,
|
||||
stage: 'idle',
|
||||
@@ -482,16 +487,14 @@ describe('applyBackendUpdate recovery', () => {
|
||||
})
|
||||
|
||||
it('waits for the backend to return after the restart drops the connection, then clears the overlay', async () => {
|
||||
updateHermesSpy.mockResolvedValue({ ok: true, name: 'update', pid: 1 })
|
||||
getActionStatusSpy.mockRejectedValue(new Error('ECONNREFUSED'))
|
||||
checkHermesUpdateSpy.mockResolvedValue({
|
||||
install_method: 'git',
|
||||
current_version: '0.16.0',
|
||||
behind: 0,
|
||||
update_available: false,
|
||||
can_apply: true,
|
||||
update_command: 'hermes update',
|
||||
message: null
|
||||
const actionId = 'd'.repeat(32)
|
||||
updateHermesSpy.mockResolvedValue({ action_id: actionId, ok: true, name: 'update', pid: 1 })
|
||||
getActionStatusSpy.mockRejectedValueOnce(new Error('ECONNREFUSED')).mockResolvedValueOnce({
|
||||
exit_code: null,
|
||||
lines: [`=== hermes-update completed ${actionId} ===`],
|
||||
name: 'update',
|
||||
pid: null,
|
||||
running: false
|
||||
})
|
||||
|
||||
const promise = applyBackendUpdate()
|
||||
@@ -504,7 +507,8 @@ describe('applyBackendUpdate recovery', () => {
|
||||
})
|
||||
|
||||
it('surfaces backend update action log lines while the action is running', async () => {
|
||||
updateHermesSpy.mockResolvedValue({ ok: true, name: 'update', pid: 1 })
|
||||
const actionId = 'e'.repeat(32)
|
||||
updateHermesSpy.mockResolvedValue({ action_id: actionId, ok: true, name: 'update', pid: 1 })
|
||||
getActionStatusSpy
|
||||
.mockResolvedValueOnce({
|
||||
exit_code: null,
|
||||
@@ -514,15 +518,13 @@ describe('applyBackendUpdate recovery', () => {
|
||||
running: true
|
||||
})
|
||||
.mockRejectedValueOnce(new Error('ECONNREFUSED'))
|
||||
checkHermesUpdateSpy.mockResolvedValue({
|
||||
install_method: 'git',
|
||||
current_version: '0.16.0',
|
||||
behind: 0,
|
||||
update_available: false,
|
||||
can_apply: true,
|
||||
update_command: 'hermes update',
|
||||
message: null
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
exit_code: null,
|
||||
lines: [`=== hermes-update completed ${actionId} ===`],
|
||||
name: 'update',
|
||||
pid: null,
|
||||
running: false
|
||||
})
|
||||
|
||||
const promise = applyBackendUpdate()
|
||||
await vi.advanceTimersByTimeAsync(1500)
|
||||
@@ -537,18 +539,323 @@ describe('applyBackendUpdate recovery', () => {
|
||||
await promise
|
||||
})
|
||||
|
||||
it('keeps waiting past the old 45-second cutoff while the update action is running', async () => {
|
||||
const actionId = 'f'.repeat(32)
|
||||
updateHermesSpy.mockResolvedValue({ action_id: actionId, ok: true, name: 'hermes-update', pid: 1 })
|
||||
|
||||
for (let attempt = 0; attempt < 31; attempt += 1) {
|
||||
getActionStatusSpy.mockResolvedValueOnce({
|
||||
exit_code: null,
|
||||
lines: ['=== hermes-update started now ===', `step ${attempt}`],
|
||||
name: 'hermes-update',
|
||||
pid: 1,
|
||||
running: true
|
||||
})
|
||||
}
|
||||
|
||||
getActionStatusSpy.mockRejectedValueOnce(new Error('ECONNREFUSED')).mockResolvedValueOnce({
|
||||
exit_code: null,
|
||||
lines: [`=== hermes-update completed ${actionId} ===`],
|
||||
name: 'hermes-update',
|
||||
pid: null,
|
||||
running: false
|
||||
})
|
||||
|
||||
const promise = applyBackendUpdate()
|
||||
await vi.advanceTimersByTimeAsync(46500)
|
||||
|
||||
expect($backendUpdateApply.get().applying).toBe(true)
|
||||
expect($backendUpdateApply.get().stage).toBe('pull')
|
||||
|
||||
await vi.advanceTimersByTimeAsync(5000)
|
||||
await expect(promise).resolves.toMatchObject({ ok: true })
|
||||
})
|
||||
|
||||
it('treats a successful no-op as complete without waiting for a restart', async () => {
|
||||
updateHermesSpy.mockResolvedValue({ ok: true, name: 'hermes-update', pid: 1 })
|
||||
getActionStatusSpy.mockResolvedValue({
|
||||
exit_code: 0,
|
||||
lines: ['stale output from another run', '=== hermes-update started now ===', '✓ Already up to date!'],
|
||||
name: 'hermes-update',
|
||||
pid: 1,
|
||||
running: false
|
||||
})
|
||||
|
||||
const promise = applyBackendUpdate()
|
||||
await vi.advanceTimersByTimeAsync(1500)
|
||||
const result = await promise
|
||||
|
||||
expect(result.ok).toBe(true)
|
||||
expect($backendUpdateApply.get().stage).toBe('idle')
|
||||
})
|
||||
|
||||
it('treats a successful dependency repair as complete without waiting for a restart', async () => {
|
||||
updateHermesSpy.mockResolvedValue({ ok: true, name: 'hermes-update', pid: 1 })
|
||||
getActionStatusSpy.mockResolvedValue({
|
||||
exit_code: 0,
|
||||
lines: ['=== hermes-update started now ===', '✓ Dependencies repaired!', '✓ Update complete!'],
|
||||
name: 'hermes-update',
|
||||
pid: 1,
|
||||
running: false
|
||||
})
|
||||
|
||||
const promise = applyBackendUpdate()
|
||||
await vi.advanceTimersByTimeAsync(1500)
|
||||
await expect(promise).resolves.toMatchObject({ ok: true })
|
||||
expect($backendUpdateApply.get().stage).toBe('idle')
|
||||
})
|
||||
|
||||
it('trusts the current action exit code without parsing its output', async () => {
|
||||
updateHermesSpy.mockResolvedValue({ ok: true, name: 'hermes-update', pid: 1 })
|
||||
getActionStatusSpy.mockResolvedValue({
|
||||
exit_code: 0,
|
||||
lines: ['✓ Already up to date!'],
|
||||
name: 'hermes-update',
|
||||
pid: 1,
|
||||
running: false
|
||||
})
|
||||
const promise = applyBackendUpdate()
|
||||
await vi.advanceTimersByTimeAsync(1500)
|
||||
await expect(promise).resolves.toMatchObject({ ok: true })
|
||||
expect(checkHermesUpdateSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('waits for current-action completion proof after the backend restarts', async () => {
|
||||
const actionId = 'a'.repeat(32)
|
||||
updateHermesSpy.mockResolvedValue({ action_id: actionId, ok: true, name: 'hermes-update', pid: 1 })
|
||||
getActionStatusSpy
|
||||
.mockRejectedValueOnce(new Error('ECONNREFUSED'))
|
||||
.mockResolvedValueOnce({
|
||||
exit_code: null,
|
||||
lines: ['Update complete!', `=== hermes-update completed ${'c'.repeat(32)} ===`],
|
||||
name: 'hermes-update',
|
||||
pid: null,
|
||||
running: false
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
exit_code: null,
|
||||
lines: ['Update complete!', `=== hermes-update completed ${actionId} ===`],
|
||||
name: 'hermes-update',
|
||||
pid: null,
|
||||
running: false
|
||||
})
|
||||
|
||||
const promise = applyBackendUpdate()
|
||||
await vi.advanceTimersByTimeAsync(5000)
|
||||
await expect(promise).resolves.toMatchObject({ ok: true })
|
||||
expect(checkHermesUpdateSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('accepts its terminal receipt when a verbose update pushes the start marker out of the log tail', async () => {
|
||||
const actionId = 'b'.repeat(32)
|
||||
updateHermesSpy.mockResolvedValue({ action_id: actionId, ok: true, name: 'hermes-update', pid: 1 })
|
||||
getActionStatusSpy.mockRejectedValueOnce(new Error('ECONNREFUSED')).mockResolvedValueOnce({
|
||||
exit_code: null,
|
||||
lines: ['final build output', 'Update complete!', `=== hermes-update completed ${actionId} ===`],
|
||||
name: 'hermes-update',
|
||||
pid: null,
|
||||
running: false
|
||||
})
|
||||
|
||||
const promise = applyBackendUpdate()
|
||||
await vi.advanceTimersByTimeAsync(5000)
|
||||
|
||||
await expect(promise).resolves.toMatchObject({ ok: true })
|
||||
expect(getActionStatusSpy).toHaveBeenCalledWith('hermes-update', 2000)
|
||||
})
|
||||
|
||||
it('proves a pre-action-ID backend reached its requested commit after restart', async () => {
|
||||
$backendUpdateStatus.set({
|
||||
behind: 2,
|
||||
commits: [{ at: 1, author: 'Nous', sha: 'requested-target', summary: 'target' }],
|
||||
fetchedAt: 1,
|
||||
supported: true,
|
||||
targetSha: 'backend:0.18.2',
|
||||
updateAvailable: true
|
||||
})
|
||||
updateHermesSpy.mockResolvedValue({ ok: true, name: 'hermes-update', pid: 1 })
|
||||
getActionStatusSpy.mockRejectedValueOnce(new Error('ECONNREFUSED')).mockResolvedValue({
|
||||
exit_code: null,
|
||||
lines: ['verbose output', 'Update complete!'],
|
||||
name: 'hermes-update',
|
||||
pid: null,
|
||||
running: false
|
||||
})
|
||||
checkHermesUpdateSpy
|
||||
.mockResolvedValueOnce({
|
||||
behind: null,
|
||||
can_apply: true,
|
||||
commits: [],
|
||||
current_version: '0.18.2',
|
||||
install_method: 'git',
|
||||
message: 'offline',
|
||||
update_available: false,
|
||||
update_command: 'hermes update'
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
behind: 1,
|
||||
can_apply: true,
|
||||
commits: [{ at: 2, author: 'Nous', sha: 'newer-commit', summary: 'newer' }],
|
||||
current_version: '0.18.2',
|
||||
install_method: 'git',
|
||||
message: null,
|
||||
update_available: true,
|
||||
update_command: 'hermes update'
|
||||
})
|
||||
|
||||
const promise = applyBackendUpdate()
|
||||
await vi.advanceTimersByTimeAsync(5000)
|
||||
|
||||
await expect(promise).resolves.toMatchObject({ ok: true })
|
||||
expect(checkHermesUpdateSpy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('proves a fast pre-action-ID packaged update by its changed version', async () => {
|
||||
$backendUpdateStatus.set({
|
||||
behind: 1,
|
||||
commits: [],
|
||||
fetchedAt: 1,
|
||||
supported: true,
|
||||
targetSha: 'backend:0.18.2',
|
||||
updateAvailable: true
|
||||
})
|
||||
updateHermesSpy.mockResolvedValue({ ok: true, name: 'hermes-update', pid: 1 })
|
||||
getActionStatusSpy.mockResolvedValue({
|
||||
exit_code: null,
|
||||
lines: ['verbose output without a retained start marker'],
|
||||
name: 'hermes-update',
|
||||
pid: null,
|
||||
running: false
|
||||
})
|
||||
checkHermesUpdateSpy.mockResolvedValue({
|
||||
behind: -1,
|
||||
can_apply: true,
|
||||
commits: [],
|
||||
current_version: '0.18.3',
|
||||
install_method: 'pip',
|
||||
message: null,
|
||||
update_available: true,
|
||||
update_command: 'hermes update'
|
||||
})
|
||||
|
||||
const promise = applyBackendUpdate()
|
||||
await vi.advanceTimersByTimeAsync(1500)
|
||||
|
||||
await expect(promise).resolves.toMatchObject({ ok: true })
|
||||
expect(checkHermesUpdateSpy).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('resumes action polling after a transient status failure', async () => {
|
||||
updateHermesSpy.mockResolvedValue({ ok: true, name: 'hermes-update', pid: 1 })
|
||||
getActionStatusSpy
|
||||
.mockRejectedValueOnce(new Error('ECONNRESET'))
|
||||
.mockResolvedValueOnce({
|
||||
exit_code: null,
|
||||
lines: ['=== hermes-update started now ===', 'still running'],
|
||||
name: 'hermes-update',
|
||||
pid: 1,
|
||||
running: true
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
exit_code: 0,
|
||||
lines: ['=== hermes-update started now ===', 'Update complete!'],
|
||||
name: 'hermes-update',
|
||||
pid: 1,
|
||||
running: false
|
||||
})
|
||||
|
||||
const promise = applyBackendUpdate()
|
||||
await vi.advanceTimersByTimeAsync(5000)
|
||||
await expect(promise).resolves.toMatchObject({ ok: true })
|
||||
expect(getActionStatusSpy).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it('restores the fixed action deadline after reconnecting', async () => {
|
||||
updateHermesSpy.mockResolvedValue({ action_id: 'a'.repeat(32), ok: true, name: 'hermes-update', pid: 1 })
|
||||
const running = {
|
||||
exit_code: null,
|
||||
lines: ['still running'],
|
||||
name: 'hermes-update',
|
||||
pid: 1,
|
||||
running: true
|
||||
}
|
||||
|
||||
for (let attempt = 0; attempt < 119; attempt += 1) {
|
||||
getActionStatusSpy.mockResolvedValueOnce(running)
|
||||
}
|
||||
getActionStatusSpy.mockRejectedValueOnce(new Error('ECONNRESET')).mockResolvedValue(running)
|
||||
|
||||
const promise = applyBackendUpdate()
|
||||
await vi.advanceTimersByTimeAsync(6 * 60 * 1000 + 1500)
|
||||
|
||||
await expect(promise).resolves.toMatchObject({ error: 'apply-failed', ok: false })
|
||||
expect($backendUpdateApply.get().stage).toBe('error')
|
||||
})
|
||||
|
||||
it('shares one in-flight update between concurrent apply requests', async () => {
|
||||
updateHermesSpy.mockResolvedValue({ ok: true, name: 'hermes-update', pid: 1 })
|
||||
getActionStatusSpy.mockResolvedValue({
|
||||
exit_code: 0,
|
||||
lines: ['=== hermes-update started now ===', '✓ Already up to date!'],
|
||||
name: 'hermes-update',
|
||||
pid: 1,
|
||||
running: false
|
||||
})
|
||||
|
||||
const first = applyBackendUpdate()
|
||||
const second = applyBackendUpdate()
|
||||
|
||||
expect(second).toBe(first)
|
||||
await vi.advanceTimersByTimeAsync(1500)
|
||||
await Promise.all([first, second])
|
||||
expect(updateHermesSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('fails closed when the update action never reaches a terminal state', async () => {
|
||||
updateHermesSpy.mockResolvedValue({ ok: true, name: 'hermes-update', pid: 1 })
|
||||
getActionStatusSpy.mockResolvedValue({
|
||||
exit_code: null,
|
||||
lines: ['=== hermes-update started now ===', 'still running'],
|
||||
name: 'hermes-update',
|
||||
pid: 1,
|
||||
running: true
|
||||
})
|
||||
|
||||
const promise = applyBackendUpdate()
|
||||
await vi.advanceTimersByTimeAsync(6 * 60 * 1000 + 1500)
|
||||
await expect(promise).resolves.toMatchObject({ ok: false, error: 'apply-failed' })
|
||||
expect($backendUpdateApply.get().stage).toBe('error')
|
||||
})
|
||||
|
||||
it('fails immediately when the update action exits nonzero', async () => {
|
||||
updateHermesSpy.mockResolvedValue({ ok: true, name: 'hermes-update', pid: 1 })
|
||||
getActionStatusSpy.mockResolvedValue({
|
||||
exit_code: 1,
|
||||
lines: ['=== hermes-update started now ===', 'update failed'],
|
||||
name: 'hermes-update',
|
||||
pid: 1,
|
||||
running: false
|
||||
})
|
||||
|
||||
const promise = applyBackendUpdate()
|
||||
await vi.advanceTimersByTimeAsync(1500)
|
||||
await expect(promise).resolves.toMatchObject({ ok: false, error: 'apply-failed' })
|
||||
expect(checkHermesUpdateSpy).not.toHaveBeenCalled()
|
||||
expect($backendUpdateApply.get().stage).toBe('error')
|
||||
})
|
||||
|
||||
it('surfaces an error when the backend never comes back after the restart', async () => {
|
||||
updateHermesSpy.mockResolvedValue({ ok: true, name: 'update', pid: 1 })
|
||||
getActionStatusSpy.mockRejectedValue(new Error('ECONNREFUSED'))
|
||||
checkHermesUpdateSpy.mockRejectedValue(new Error('ECONNREFUSED'))
|
||||
|
||||
const promise = applyBackendUpdate()
|
||||
await vi.advanceTimersByTimeAsync(70000)
|
||||
await vi.advanceTimersByTimeAsync(250000)
|
||||
const result = await promise
|
||||
|
||||
expect(result.ok).toBe(false)
|
||||
expect($backendUpdateApply.get().stage).toBe('error')
|
||||
})
|
||||
}, 10000)
|
||||
})
|
||||
|
||||
describe('startUpdatePoller', () => {
|
||||
|
||||
@@ -487,24 +487,9 @@ export async function applyUpdates(opts: DesktopUpdateApplyOptions = {}): Promis
|
||||
}
|
||||
}
|
||||
|
||||
const BACKEND_RETURN_POLL_MS = 1500
|
||||
const BACKEND_RETURN_MAX_ATTEMPTS = 40
|
||||
|
||||
async function waitForBackendReturn(): Promise<boolean> {
|
||||
for (let attempt = 0; attempt < BACKEND_RETURN_MAX_ATTEMPTS; attempt += 1) {
|
||||
await new Promise(resolve => globalThis.setTimeout(resolve, BACKEND_RETURN_POLL_MS))
|
||||
|
||||
try {
|
||||
await checkHermesUpdate()
|
||||
|
||||
return true
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
const BACKEND_ACTION_POLL_MS = 1500
|
||||
const BACKEND_ACTION_MAX_MS = 6 * 60 * 1000
|
||||
const BACKEND_RETURN_MAX_MS = 4 * 60 * 1000
|
||||
|
||||
function finishBackendApply(returned: boolean): DesktopUpdateApplyResult {
|
||||
if (returned) {
|
||||
@@ -547,7 +532,32 @@ function ingestBackendActionStatus(status: Awaited<ReturnType<typeof getActionSt
|
||||
})
|
||||
}
|
||||
|
||||
export async function applyBackendUpdate(): Promise<DesktopUpdateApplyResult> {
|
||||
function completedAfterRestart(
|
||||
status: Awaited<ReturnType<typeof getActionStatus>>,
|
||||
actionId: string | undefined
|
||||
): boolean {
|
||||
return !!actionId && status.lines.some(line => line === `=== hermes-update completed ${actionId} ===`)
|
||||
}
|
||||
|
||||
function legacyBackendReachedTarget(
|
||||
status: BackendUpdateCheckResponse,
|
||||
targetSha: string | undefined,
|
||||
previousVersion: string | undefined
|
||||
): boolean {
|
||||
if (status.behind === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (previousVersion && status.current_version !== previousVersion) {
|
||||
return true
|
||||
}
|
||||
|
||||
return !!targetSha && !!status.commits?.length && !status.commits.some(commit => commit.sha === targetSha)
|
||||
}
|
||||
|
||||
let backendUpdateInFlight: Promise<DesktopUpdateApplyResult> | null = null
|
||||
|
||||
async function runBackendUpdate(): Promise<DesktopUpdateApplyResult> {
|
||||
dismissNotification(UPDATE_TOAST_ID)
|
||||
$backendUpdateApply.set({
|
||||
...IDLE,
|
||||
@@ -557,6 +567,11 @@ export async function applyBackendUpdate(): Promise<DesktopUpdateApplyResult> {
|
||||
})
|
||||
|
||||
try {
|
||||
const previousStatus = $backendUpdateStatus.get()
|
||||
const requestedTargetSha = previousStatus?.commits?.at(0)?.sha
|
||||
const previousVersion = previousStatus?.targetSha?.startsWith('backend:')
|
||||
? previousStatus.targetSha.slice('backend:'.length)
|
||||
: undefined
|
||||
const started = await updateHermes()
|
||||
|
||||
if (!started.ok) {
|
||||
@@ -575,43 +590,69 @@ export async function applyBackendUpdate(): Promise<DesktopUpdateApplyResult> {
|
||||
})
|
||||
|
||||
let last: Awaited<ReturnType<typeof getActionStatus>> | null = null
|
||||
// Backups, dependency repair, and builds can legitimately take several
|
||||
// minutes. Keep the generous cap only as a guard against a stuck action.
|
||||
const actionDeadline = Date.now() + BACKEND_ACTION_MAX_MS
|
||||
let deadline = actionDeadline
|
||||
let reconnecting = false
|
||||
|
||||
for (let attempt = 0; attempt < 30; attempt += 1) {
|
||||
await new Promise(resolve => globalThis.setTimeout(resolve, 1500))
|
||||
while (Date.now() < deadline) {
|
||||
await new Promise(resolve => globalThis.setTimeout(resolve, BACKEND_ACTION_POLL_MS))
|
||||
|
||||
try {
|
||||
last = await getActionStatus(started.name, 200)
|
||||
last = await getActionStatus(started.name, 2000)
|
||||
ingestBackendActionStatus(last)
|
||||
} catch {
|
||||
// The dashboard restarts mid-update, dropping this connection — expected, not a failure.
|
||||
$backendUpdateApply.set({
|
||||
...$backendUpdateApply.get(),
|
||||
applying: true,
|
||||
stage: 'restart',
|
||||
message: translateNow('updates.applyStatus.restarting')
|
||||
})
|
||||
if (!reconnecting) {
|
||||
reconnecting = true
|
||||
deadline = Date.now() + BACKEND_RETURN_MAX_MS
|
||||
$backendUpdateApply.set({
|
||||
...$backendUpdateApply.get(),
|
||||
applying: true,
|
||||
stage: 'restart',
|
||||
message: translateNow('updates.applyStatus.restarting')
|
||||
})
|
||||
}
|
||||
|
||||
return finishBackendApply(await waitForBackendReturn())
|
||||
continue
|
||||
}
|
||||
|
||||
if (last && !last.running) {
|
||||
if (last.running) {
|
||||
if (reconnecting) {
|
||||
reconnecting = false
|
||||
deadline = actionDeadline
|
||||
$backendUpdateApply.set({
|
||||
...$backendUpdateApply.get(),
|
||||
applying: true,
|
||||
stage: 'pull',
|
||||
message: translateNow('updates.applyStatus.pulling')
|
||||
})
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (last.exit_code === 0 || (last.exit_code === null && completedAfterRestart(last, started.action_id))) {
|
||||
return finishBackendApply(true)
|
||||
}
|
||||
|
||||
if (!started.action_id && last.exit_code === null) {
|
||||
try {
|
||||
const status = await checkHermesUpdate(true)
|
||||
|
||||
if (legacyBackendReachedTarget(status, requestedTargetSha, previousVersion)) {
|
||||
return finishBackendApply(true)
|
||||
}
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (last.exit_code !== null) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const ok = !!last && (last.exit_code ?? 1) === 0
|
||||
|
||||
if (ok) {
|
||||
$backendUpdateApply.set({
|
||||
...$backendUpdateApply.get(),
|
||||
applying: true,
|
||||
stage: 'restart',
|
||||
message: translateNow('updates.applyStatus.restarting')
|
||||
})
|
||||
|
||||
return finishBackendApply(await waitForBackendReturn())
|
||||
}
|
||||
|
||||
$backendUpdateApply.set({
|
||||
...$backendUpdateApply.get(),
|
||||
applying: false,
|
||||
@@ -635,6 +676,18 @@ export async function applyBackendUpdate(): Promise<DesktopUpdateApplyResult> {
|
||||
}
|
||||
}
|
||||
|
||||
export function applyBackendUpdate(): Promise<DesktopUpdateApplyResult> {
|
||||
if (backendUpdateInFlight) {
|
||||
return backendUpdateInFlight
|
||||
}
|
||||
|
||||
backendUpdateInFlight = runBackendUpdate().finally(() => {
|
||||
backendUpdateInFlight = null
|
||||
})
|
||||
|
||||
return backendUpdateInFlight
|
||||
}
|
||||
|
||||
function ingestProgress(payload: DesktopUpdateProgress): void {
|
||||
const current = $updateApply.get()
|
||||
const log = [...current.log, { stage: payload.stage, message: payload.message, at: payload.at }].slice(-50)
|
||||
|
||||
@@ -1144,6 +1144,8 @@ export interface ActionResponse {
|
||||
name: string
|
||||
ok: boolean
|
||||
pid: number
|
||||
action_id?: string
|
||||
already_running?: boolean
|
||||
}
|
||||
|
||||
export interface ActionStatusResponse {
|
||||
|
||||
Reference in New Issue
Block a user