[codex] fix self update restart (#707)

* fix: make self update restart reliably

* chore: clarify update success message
This commit is contained in:
ekko
2026-05-14 12:06:39 +08:00
committed by GitHub
parent b2a80808e3
commit 9b243cab36
13 changed files with 121 additions and 37 deletions
+42 -5
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env node
import { spawn, execSync } from 'child_process'
import { resolve, dirname, join } from 'path'
import { spawn, execSync, execFileSync } from 'child_process'
import { resolve, dirname, join, delimiter } from 'path'
import { fileURLToPath } from 'url'
import { readFileSync, writeFileSync, unlinkSync, mkdirSync, openSync, chmodSync, statSync, existsSync } from 'fs'
import { randomBytes } from 'crypto'
@@ -56,8 +56,27 @@ function getNpmBin() {
return join(getNodeBinDir(), process.platform === 'win32' ? 'npm.cmd' : 'npm')
}
function getCliBin() {
return join(getNodeBinDir(), process.platform === 'win32' ? 'hermes-web-ui.cmd' : 'hermes-web-ui')
function getCurrentNodeEnv() {
return {
...process.env,
PATH: [getNodeBinDir(), process.env.PATH].filter(Boolean).join(delimiter),
npm_node_execpath: process.execPath,
}
}
function getGlobalPrefix() {
return execFileSync(getNpmBin(), ['prefix', '-g'], {
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe'],
env: getCurrentNodeEnv(),
}).trim()
}
function getGlobalCliBin() {
const prefix = getGlobalPrefix()
return process.platform === 'win32'
? join(prefix, 'hermes-web-ui.cmd')
: join(prefix, 'bin', 'hermes-web-ui')
}
function getWindowsShell() {
@@ -431,18 +450,36 @@ function doUpdate() {
const child = spawnCli(getNpmBin(), ['install', '-g', 'hermes-web-ui@latest'], {
stdio: 'inherit',
windowsHide: true,
env: getCurrentNodeEnv(),
})
child.on('error', (err) => {
console.log(` ✗ Update failed: ${err.message}`)
process.exit(1)
})
child.on('exit', (code) => {
if (code === 0) {
console.log(' ✓ Update complete, restarting...')
const restart = spawnCli(getCliBin(), ['restart', '--port', String(getUpdatePort())], {
const cli = getGlobalCliBin()
if (!existsSync(cli)) {
console.log(` ✗ Updated CLI not found: ${cli}`)
process.exit(1)
}
const restart = spawnCli(cli, ['restart', '--port', String(getUpdatePort())], {
stdio: 'inherit',
windowsHide: true,
env: getCurrentNodeEnv(),
})
restart.on('error', (err) => {
console.log(` ✗ Restart failed: ${err.message}`)
process.exit(1)
})
restart.on('exit', (restartCode) => process.exit(restartCode ?? 1))
} else {
console.log(' ✗ Update failed')
process.exit(code ?? 1)
}
})
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hermes-web-ui",
"version": "0.5.20",
"version": "0.5.21",
"description": "Self-hosted AI chat dashboard for Hermes Agent — multi-model web UI with multi-platform integration",
"repository": {
"type": "git",
+1 -1
View File
@@ -6,7 +6,7 @@ export interface ChangelogEntry {
export const changelog: ChangelogEntry[] = [
{
version: '0.5.20',
version: '0.5.21',
date: '2026-05-14',
changes: [
'changelog.new_0_5_18_1',
+1 -1
View File
@@ -92,7 +92,7 @@ export default {
updateVersion: 'Aktualisieren auf v{version}',
reloadClientVersion: 'Für v{version} neu laden',
updating: 'Aktualisierung...',
updateSuccess: 'Aktualisierung abgeschlossen, bitte Server neu starten',
updateSuccess: 'Aktualisierung erfolgreich. Bitte aktualisieren Sie die Seite in Kurze. Wenn der Dienst langere Zeit nicht startet, starten Sie ihn manuell.',
updateFailed: 'Aktualisierung fehlgeschlagen',
logout: 'Abmelden',
nodeVersionWarning: 'Node.js v{version} erkannt. Bitte aktualisieren Sie auf Version 23 oder neuer.',
+1 -1
View File
@@ -104,7 +104,7 @@ export default {
updateVersion: 'Upgrade to v{version}',
reloadClientVersion: 'Reload for v{version}',
updating: 'Updating...',
updateSuccess: 'Update complete, please restart the server',
updateSuccess: 'Update successful. Please refresh the page shortly. If it does not start after a while, start it manually.',
updateFailed: 'Update failed',
logout: 'Sign Out',
nodeVersionWarning: 'Detected Node.js v{version}. Please upgrade to version 23 or later.',
+1 -1
View File
@@ -92,7 +92,7 @@ export default {
updateVersion: 'Actualizar a v{version}',
reloadClientVersion: 'Recargar para v{version}',
updating: 'Actualizando...',
updateSuccess: 'Actualizacion completa, por favor reinicia el servidor',
updateSuccess: 'Actualizacion completada. Actualiza la pagina en breve. Si no se inicia despues de un tiempo, inicialo manualmente.',
updateFailed: 'Error al actualizar',
logout: 'Cerrar sesion',
nodeVersionWarning: 'Se detecto Node.js v{version}. Actualiza a la version 23 o posterior.',
+1 -1
View File
@@ -92,7 +92,7 @@ export default {
updateVersion: 'Mettre a jour vers v{version}',
reloadClientVersion: 'Recharger pour v{version}',
updating: 'Mise a jour...',
updateSuccess: 'Mise a jour terminee, veuillez redemarrer le serveur',
updateSuccess: 'Mise a jour terminee. Veuillez actualiser la page sous peu. Si le service ne demarre pas apres un moment, demarrez-le manuellement.',
updateFailed: 'Echec de la mise a jour',
logout: 'Deconnexion',
nodeVersionWarning: 'Node.js v{version} detecte. Veuillez passer a la version 23 ou ulterieure.',
+1 -1
View File
@@ -92,7 +92,7 @@ export default {
updateVersion: 'v{version} にアップグレード',
reloadClientVersion: 'v{version} に再読み込み',
updating: '更新中...',
updateSuccess: '更新が完了しました。サーバーを再起動してください',
updateSuccess: '更新が完了しました。しばらくしてからページを再読み込みしてください。長時間起動しない場合は手動で起動してください',
updateFailed: '更新に失敗しました',
logout: 'ログアウト',
nodeVersionWarning: 'Node.js v{version} が検出されました。バージョン23以降にアップグレードしてください。',
+1 -1
View File
@@ -92,7 +92,7 @@ export default {
updateVersion: 'v{version}(으)로 업그레이드',
reloadClientVersion: 'v{version}(으)로 새로고침',
updating: '업데이트 중...',
updateSuccess: '업데이트 완료, 서버를 재시작해 주세요',
updateSuccess: '업데이트 완료되었습니다. 잠시 후 페이지를 새로고침하세요. 오랫동안 시작되지 않으면 수동으로 시작하세요.',
updateFailed: '업데이트 실패',
logout: '로그아웃',
nodeVersionWarning: 'Node.js v{version}이 감지되었습니다. 버전 23 이상으로 업그레이드하세요.',
+1 -1
View File
@@ -92,7 +92,7 @@ export default {
updateVersion: 'Atualizar para v{version}',
reloadClientVersion: 'Recarregar para v{version}',
updating: 'Atualizando...',
updateSuccess: 'Atualizacao concluida, por favor reinicie o servidor',
updateSuccess: 'Atualizacao concluida. Atualize a pagina em breve. Se nao iniciar apos algum tempo, inicie manualmente.',
updateFailed: 'Falha na atualizacao',
logout: 'Sair',
nodeVersionWarning: 'Node.js v{version} detectado. Atualize para a versao 23 ou posterior.',
+1 -1
View File
@@ -104,7 +104,7 @@ export default {
updateVersion: '升級版本 v{version}',
reloadClientVersion: '重新整理到 v{version}',
updating: '正在更新...',
updateSuccess: '更新完成,請重新啟動服務',
updateSuccess: '更新成功,請稍後重新整理頁面,如長時間未啟動,請手動啟動',
updateFailed: '更新失敗',
logout: '登出',
nodeVersionWarning: '偵測到 Node.js v{version},請升級至 23 以上版本。',
+1 -1
View File
@@ -104,7 +104,7 @@ export default {
updateVersion: '升级版本 v{version}',
reloadClientVersion: '刷新到 v{version}',
updating: '正在更新...',
updateSuccess: '更新完成,请重启服务',
updateSuccess: '更新成功,请稍后刷新页面,如长时间未启动,请手动启动',
updateFailed: '更新失败',
logout: '退出登录',
nodeVersionWarning: '检测到 Node.js v{version},请升级到23以上版本。',
+68 -21
View File
@@ -2,6 +2,8 @@ import { execFileSync, spawn } from 'child_process'
import { existsSync } from 'fs'
import { delimiter, dirname, join } from 'path'
let updateInProgress = false
function getNodeBinDir() {
return dirname(process.execPath)
}
@@ -10,27 +12,39 @@ function getNodePrefix() {
return process.platform === 'win32' ? getNodeBinDir() : dirname(getNodeBinDir())
}
function getNpmCliPath() {
function getHomebrewPrefix() {
const match = process.execPath.match(/^(.*)\/Cellar\/[^/]+\/[^/]+\/bin\/node$/)
return match?.[1] || null
}
function getNpmCliCandidates() {
const prefix = getNodePrefix()
const candidates = process.platform === 'win32'
const homebrewPrefix = getHomebrewPrefix()
return process.platform === 'win32'
? [
join(prefix, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
join(getNodeBinDir(), 'node_modules', 'npm', 'bin', 'npm-cli.js'),
]
: [join(prefix, 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js')]
const npmCli = candidates.find(existsSync)
if (!npmCli) {
throw new Error(`Unable to locate npm CLI for ${process.execPath}; checked ${candidates.join(', ')}`)
}
return npmCli
: [
join(prefix, 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
...(homebrewPrefix ? [join(homebrewPrefix, 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js')] : []),
]
}
function getGlobalPackageBin(prefix: string) {
return process.platform === 'win32'
? join(prefix, 'node_modules', 'hermes-web-ui', 'bin', 'hermes-web-ui.mjs')
: join(prefix, 'lib', 'node_modules', 'hermes-web-ui', 'bin', 'hermes-web-ui.mjs')
function getNpmCliPath() {
const candidates = getNpmCliCandidates()
const npmCli = candidates.find(existsSync)
return npmCli || null
}
function getNpmBin() {
return process.platform === 'win32' ? 'npm.cmd' : 'npm'
}
function getGlobalPackageBin(root: string) {
return join(root, 'hermes-web-ui', 'bin', 'hermes-web-ui.mjs')
}
function getCurrentNodeEnv() {
@@ -42,7 +56,11 @@ function getCurrentNodeEnv() {
}
function runNpm(args: string[], options: { timeout?: number } = {}) {
return execFileSync(process.execPath, [getNpmCliPath(), ...args], {
const npmCli = getNpmCliPath()
const command = npmCli ? process.execPath : getNpmBin()
const commandArgs = npmCli ? [npmCli, ...args] : args
return execFileSync(command, commandArgs, {
encoding: 'utf-8',
timeout: options.timeout,
stdio: ['pipe', 'pipe', 'pipe'],
@@ -50,12 +68,16 @@ function runNpm(args: string[], options: { timeout?: number } = {}) {
}).trim()
}
function getGlobalPrefix() {
return runNpm(['prefix', '-g'])
function getGlobalRoot() {
return runNpm(['root', '-g'])
}
function getGlobalCliScript() {
return getGlobalPackageBin(getGlobalPrefix())
const cli = getGlobalPackageBin(getGlobalRoot())
if (!existsSync(cli)) {
throw new Error(`Updated hermes-web-ui CLI not found: ${cli}`)
}
return cli
}
function runUpdateInstall() {
@@ -74,6 +96,17 @@ function spawnRestart(port: string) {
}
export async function handleUpdate(ctx: any) {
if (updateInProgress) {
ctx.status = 409
ctx.body = {
success: false,
message: 'hermes-web-ui update is already in progress',
}
return
}
updateInProgress = true
try {
const output = runUpdateInstall()
@@ -83,13 +116,27 @@ export async function handleUpdate(ctx: any) {
}
setTimeout(() => {
let restart
try {
spawnRestart(process.env.PORT || '8648').unref()
} finally {
process.exit(0)
restart = spawnRestart(process.env.PORT || '8648')
} catch (err) {
updateInProgress = false
console.error('[update] failed to spawn restart:', err)
return
}
restart.on('error', (err) => {
updateInProgress = false
console.error('[update] restart process failed:', err)
})
restart.on('exit', (code, signal) => {
updateInProgress = false
console.error(`[update] restart process exited before replacing server: code=${code} signal=${signal}`)
})
restart.unref()
}, 3000)
} catch (err: any) {
updateInProgress = false
ctx.status = 500
ctx.body = {
success: false,