Эх сурвалжийг харах

修复慢速LaunchAgent注销升级失败

samien 2 сар өмнө
parent
commit
52fda79feb

+ 13 - 4
bin/hermes-yaoyao.mjs

@@ -115,15 +115,24 @@ function launchctlBootstrapRetryable(error) {
 export async function bootstrapLaunchAgent(
   runCommand = run,
   wait = delay,
-  retryDelays = [250, 500, 1_000, 1_500],
+  options = {},
 ) {
-  for (let attempt = 0; ; attempt += 1) {
+  const timeoutMs = options.timeoutMs ?? 20_000
+  const initialDelayMs = options.initialDelayMs ?? 250
+  const maximumDelayMs = options.maximumDelayMs ?? 2_000
+  const now = options.now ?? Date.now
+  const deadline = now() + timeoutMs
+  let retryDelayMs = initialDelayMs
+  for (;;) {
     try {
       return runCommand('launchctl', ['bootstrap', domain, plistPath])
     } catch (error) {
-      const waitMs = retryDelays[attempt]
-      if (waitMs === undefined || !launchctlBootstrapRetryable(error)) throw error
+      if (!launchctlBootstrapRetryable(error)) throw error
+      const remainingMs = deadline - now()
+      if (remainingMs <= 0) throw error
+      const waitMs = Math.min(retryDelayMs, maximumDelayMs, remainingMs)
       await wait(waitMs)
+      retryDelayMs = Math.min(retryDelayMs * 2, maximumDelayMs)
     }
   }
 }

+ 35 - 0
tests/serviceCli.test.ts

@@ -48,6 +48,41 @@ describe('hermes-yaoyao LaunchAgent plist', () => {
     expect(wait).toHaveBeenCalledWith(250)
   })
 
+  it('keeps retrying until a slow launchd removal finishes', async () => {
+    const error = Object.assign(new Error('launchctl bootstrap failed'), {
+      stderr: 'Bootstrap failed: 5: Input/output error',
+    })
+    let elapsedMs = 0
+    const run = vi.fn(() => {
+      if (elapsedMs < 5_000) throw error
+      return ''
+    })
+    const wait = vi.fn(async (waitMs: number) => { elapsedMs += waitMs })
+
+    await expect(bootstrapLaunchAgent(run, wait, {
+      timeoutMs: 20_000,
+      now: () => elapsedMs,
+    })).resolves.toBe('')
+    expect(elapsedMs).toBeGreaterThanOrEqual(5_000)
+    expect(run).toHaveBeenCalledTimes(6)
+  })
+
+  it('stops retrying a transient launchctl error at the deadline', async () => {
+    const error = Object.assign(new Error('launchctl bootstrap failed'), {
+      stderr: 'Bootstrap failed: 5: Input/output error',
+    })
+    let elapsedMs = 0
+    const run = vi.fn(() => { throw error })
+    const wait = vi.fn(async (waitMs: number) => { elapsedMs += waitMs })
+
+    await expect(bootstrapLaunchAgent(run, wait, {
+      timeoutMs: 2_000,
+      now: () => elapsedMs,
+    })).rejects.toThrow(error)
+    expect(elapsedMs).toBe(2_000)
+    expect(run).toHaveBeenCalledTimes(5)
+  })
+
   it('does not retry permanent launchctl bootstrap failures', async () => {
     const error = new Error('Bootstrap failed: 125: Domain does not support specified action')
     const run = vi.fn(() => { throw error })