|
@@ -109,7 +109,9 @@ export class LaunchAgentService {
|
|
|
catch { return new Set() }
|
|
catch { return new Set() }
|
|
|
}
|
|
}
|
|
|
listeners(established = false) {
|
|
listeners(established = false) {
|
|
|
- try { return new Set(execFileSync('/usr/sbin/lsof', ['-nP', `-iTCP:${this.port}`, '-sTCP:' + (established ? 'ESTABLISHED' : 'LISTEN'), '-t'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim().split(/\s+/).map(Number)) }
|
|
|
|
|
|
|
+ const pid = established ? this.pid() : undefined
|
|
|
|
|
+ if (established && !pid) return new Set()
|
|
|
|
|
+ try { return new Set(execFileSync('/usr/sbin/lsof', [...(pid ? ['-a', '-p', String(pid)] : []), '-nP', `-iTCP:${this.port}`, '-sTCP:' + (established ? 'ESTABLISHED' : 'LISTEN'), '-t'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim().split(/\s+/).map(Number)) }
|
|
|
catch { return new Set() }
|
|
catch { return new Set() }
|
|
|
}
|
|
}
|
|
|
record() {
|
|
record() {
|
|
@@ -139,9 +141,10 @@ export class LaunchAgentService {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
onProgress('正在等待聊天和后台任务结束后同步 Web…')
|
|
onProgress('正在等待聊天和后台任务结束后同步 Web…')
|
|
|
|
|
+ if (Date.now() >= deadline) break
|
|
|
await sleep(1000)
|
|
await sleep(1000)
|
|
|
} while (Date.now() < deadline)
|
|
} while (Date.now() < deadline)
|
|
|
- throw new Error('当前仍有任务或旧版客户端连接,已延后同步;任务结束后可重试')
|
|
|
|
|
|
|
+ throw Object.assign(new Error('当前仍有任务或旧版客户端连接,已延后同步;任务结束并关闭其他客户端后可重试'), { code: 'service_busy' })
|
|
|
}
|
|
}
|
|
|
async resume() {
|
|
async resume() {
|
|
|
const record = this.record()
|
|
const record = this.record()
|
|
@@ -234,7 +237,7 @@ export async function recoverTransition({ home, releaseRoot, driver, onProgress
|
|
|
rmSync(path)
|
|
rmSync(path)
|
|
|
await driver.resume()
|
|
await driver.resume()
|
|
|
}
|
|
}
|
|
|
-export async function transitionService({ home, releaseRoot, driver, finalRoot, onProgress = () => {}, rollback = false }) {
|
|
|
|
|
|
|
+export async function transitionService({ home, releaseRoot, driver, finalRoot, onProgress = () => {}, rollback = false, waitForIdleMs }) {
|
|
|
const previous = driver.snapshot(), previousCurrent = currentRelease(releaseRoot)
|
|
const previous = driver.snapshot(), previousCurrent = currentRelease(releaseRoot)
|
|
|
const path = join(home, 'updates', 'transition.json'), id = randomUUID()
|
|
const path = join(home, 'updates', 'transition.json'), id = randomUUID()
|
|
|
const success = join(home, 'updates', 'last-success.json')
|
|
const success = join(home, 'updates', 'last-success.json')
|
|
@@ -243,7 +246,7 @@ export async function transitionService({ home, releaseRoot, driver, finalRoot,
|
|
|
const expected = readRuntime(finalRoot)
|
|
const expected = readRuntime(finalRoot)
|
|
|
try {
|
|
try {
|
|
|
if (previous?.wasRunning) await driver.verify(undefined, 5000)
|
|
if (previous?.wasRunning) await driver.verify(undefined, 5000)
|
|
|
- await driver.quiesce(onProgress)
|
|
|
|
|
|
|
+ await driver.quiesce(onProgress, waitForIdleMs)
|
|
|
onProgress('正在备份数据并切换 Web 服务…')
|
|
onProgress('正在备份数据并切换 Web 服务…')
|
|
|
// Persist before stopping; recovery never mistakes a partial backup for a complete snapshot.
|
|
// Persist before stopping; recovery never mistakes a partial backup for a complete snapshot.
|
|
|
writeJSON(path, journal)
|
|
writeJSON(path, journal)
|
|
@@ -306,7 +309,7 @@ export async function synchronizeDesktop({ home, runtimeRoot, releaseRoot, drive
|
|
|
finally { rmSync(staging, { recursive: true, force: true }) }
|
|
finally { rmSync(staging, { recursive: true, force: true }) }
|
|
|
}
|
|
}
|
|
|
verifyRuntimePackage(finalRoot)
|
|
verifyRuntimePackage(finalRoot)
|
|
|
- await transitionService({ home, releaseRoot, driver, finalRoot, onProgress })
|
|
|
|
|
|
|
+ await transitionService({ home, releaseRoot, driver, finalRoot, onProgress, waitForIdleMs: 0 })
|
|
|
writeJSON(marker, { artifactDigest: target.artifactDigest, action: decision, completedAt: new Date().toISOString() })
|
|
writeJSON(marker, { artifactDigest: target.artifactDigest, action: decision, completedAt: new Date().toISOString() })
|
|
|
return { action: decision, current: target }
|
|
return { action: decision, current: target }
|
|
|
} finally { release?.(); unlock() }
|
|
} finally { release?.(); unlock() }
|