[codex] fix self update restart (#707)
* fix: make self update restart reliably * chore: clarify update success message
This commit is contained in:
+42
-5
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import { spawn, execSync } from 'child_process'
|
import { spawn, execSync, execFileSync } from 'child_process'
|
||||||
import { resolve, dirname, join } from 'path'
|
import { resolve, dirname, join, delimiter } from 'path'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
import { readFileSync, writeFileSync, unlinkSync, mkdirSync, openSync, chmodSync, statSync, existsSync } from 'fs'
|
import { readFileSync, writeFileSync, unlinkSync, mkdirSync, openSync, chmodSync, statSync, existsSync } from 'fs'
|
||||||
import { randomBytes } from 'crypto'
|
import { randomBytes } from 'crypto'
|
||||||
@@ -56,8 +56,27 @@ function getNpmBin() {
|
|||||||
return join(getNodeBinDir(), process.platform === 'win32' ? 'npm.cmd' : 'npm')
|
return join(getNodeBinDir(), process.platform === 'win32' ? 'npm.cmd' : 'npm')
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCliBin() {
|
function getCurrentNodeEnv() {
|
||||||
return join(getNodeBinDir(), process.platform === 'win32' ? 'hermes-web-ui.cmd' : 'hermes-web-ui')
|
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() {
|
function getWindowsShell() {
|
||||||
@@ -431,18 +450,36 @@ function doUpdate() {
|
|||||||
const child = spawnCli(getNpmBin(), ['install', '-g', 'hermes-web-ui@latest'], {
|
const child = spawnCli(getNpmBin(), ['install', '-g', 'hermes-web-ui@latest'], {
|
||||||
stdio: 'inherit',
|
stdio: 'inherit',
|
||||||
windowsHide: true,
|
windowsHide: true,
|
||||||
|
env: getCurrentNodeEnv(),
|
||||||
|
})
|
||||||
|
|
||||||
|
child.on('error', (err) => {
|
||||||
|
console.log(` ✗ Update failed: ${err.message}`)
|
||||||
|
process.exit(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
child.on('exit', (code) => {
|
child.on('exit', (code) => {
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
console.log(' ✓ Update complete, restarting...')
|
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',
|
stdio: 'inherit',
|
||||||
windowsHide: true,
|
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))
|
restart.on('exit', (restartCode) => process.exit(restartCode ?? 1))
|
||||||
} else {
|
} else {
|
||||||
console.log(' ✗ Update failed')
|
console.log(' ✗ Update failed')
|
||||||
|
process.exit(code ?? 1)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hermes-web-ui",
|
"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",
|
"description": "Self-hosted AI chat dashboard for Hermes Agent — multi-model web UI with multi-platform integration",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export interface ChangelogEntry {
|
|||||||
|
|
||||||
export const changelog: ChangelogEntry[] = [
|
export const changelog: ChangelogEntry[] = [
|
||||||
{
|
{
|
||||||
version: '0.5.20',
|
version: '0.5.21',
|
||||||
date: '2026-05-14',
|
date: '2026-05-14',
|
||||||
changes: [
|
changes: [
|
||||||
'changelog.new_0_5_18_1',
|
'changelog.new_0_5_18_1',
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export default {
|
|||||||
updateVersion: 'Aktualisieren auf v{version}',
|
updateVersion: 'Aktualisieren auf v{version}',
|
||||||
reloadClientVersion: 'Für v{version} neu laden',
|
reloadClientVersion: 'Für v{version} neu laden',
|
||||||
updating: 'Aktualisierung...',
|
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',
|
updateFailed: 'Aktualisierung fehlgeschlagen',
|
||||||
logout: 'Abmelden',
|
logout: 'Abmelden',
|
||||||
nodeVersionWarning: 'Node.js v{version} erkannt. Bitte aktualisieren Sie auf Version 23 oder neuer.',
|
nodeVersionWarning: 'Node.js v{version} erkannt. Bitte aktualisieren Sie auf Version 23 oder neuer.',
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export default {
|
|||||||
updateVersion: 'Upgrade to v{version}',
|
updateVersion: 'Upgrade to v{version}',
|
||||||
reloadClientVersion: 'Reload for v{version}',
|
reloadClientVersion: 'Reload for v{version}',
|
||||||
updating: 'Updating...',
|
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',
|
updateFailed: 'Update failed',
|
||||||
logout: 'Sign Out',
|
logout: 'Sign Out',
|
||||||
nodeVersionWarning: 'Detected Node.js v{version}. Please upgrade to version 23 or later.',
|
nodeVersionWarning: 'Detected Node.js v{version}. Please upgrade to version 23 or later.',
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export default {
|
|||||||
updateVersion: 'Actualizar a v{version}',
|
updateVersion: 'Actualizar a v{version}',
|
||||||
reloadClientVersion: 'Recargar para v{version}',
|
reloadClientVersion: 'Recargar para v{version}',
|
||||||
updating: 'Actualizando...',
|
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',
|
updateFailed: 'Error al actualizar',
|
||||||
logout: 'Cerrar sesion',
|
logout: 'Cerrar sesion',
|
||||||
nodeVersionWarning: 'Se detecto Node.js v{version}. Actualiza a la version 23 o posterior.',
|
nodeVersionWarning: 'Se detecto Node.js v{version}. Actualiza a la version 23 o posterior.',
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export default {
|
|||||||
updateVersion: 'Mettre a jour vers v{version}',
|
updateVersion: 'Mettre a jour vers v{version}',
|
||||||
reloadClientVersion: 'Recharger pour v{version}',
|
reloadClientVersion: 'Recharger pour v{version}',
|
||||||
updating: 'Mise a jour...',
|
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',
|
updateFailed: 'Echec de la mise a jour',
|
||||||
logout: 'Deconnexion',
|
logout: 'Deconnexion',
|
||||||
nodeVersionWarning: 'Node.js v{version} detecte. Veuillez passer a la version 23 ou ulterieure.',
|
nodeVersionWarning: 'Node.js v{version} detecte. Veuillez passer a la version 23 ou ulterieure.',
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export default {
|
|||||||
updateVersion: 'v{version} にアップグレード',
|
updateVersion: 'v{version} にアップグレード',
|
||||||
reloadClientVersion: 'v{version} に再読み込み',
|
reloadClientVersion: 'v{version} に再読み込み',
|
||||||
updating: '更新中...',
|
updating: '更新中...',
|
||||||
updateSuccess: '更新が完了しました。サーバーを再起動してください',
|
updateSuccess: '更新が完了しました。しばらくしてからページを再読み込みしてください。長時間起動しない場合は手動で起動してください。',
|
||||||
updateFailed: '更新に失敗しました',
|
updateFailed: '更新に失敗しました',
|
||||||
logout: 'ログアウト',
|
logout: 'ログアウト',
|
||||||
nodeVersionWarning: 'Node.js v{version} が検出されました。バージョン23以降にアップグレードしてください。',
|
nodeVersionWarning: 'Node.js v{version} が検出されました。バージョン23以降にアップグレードしてください。',
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export default {
|
|||||||
updateVersion: 'v{version}(으)로 업그레이드',
|
updateVersion: 'v{version}(으)로 업그레이드',
|
||||||
reloadClientVersion: 'v{version}(으)로 새로고침',
|
reloadClientVersion: 'v{version}(으)로 새로고침',
|
||||||
updating: '업데이트 중...',
|
updating: '업데이트 중...',
|
||||||
updateSuccess: '업데이트 완료, 서버를 재시작해 주세요',
|
updateSuccess: '업데이트가 완료되었습니다. 잠시 후 페이지를 새로고침하세요. 오랫동안 시작되지 않으면 수동으로 시작하세요.',
|
||||||
updateFailed: '업데이트 실패',
|
updateFailed: '업데이트 실패',
|
||||||
logout: '로그아웃',
|
logout: '로그아웃',
|
||||||
nodeVersionWarning: 'Node.js v{version}이 감지되었습니다. 버전 23 이상으로 업그레이드하세요.',
|
nodeVersionWarning: 'Node.js v{version}이 감지되었습니다. 버전 23 이상으로 업그레이드하세요.',
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export default {
|
|||||||
updateVersion: 'Atualizar para v{version}',
|
updateVersion: 'Atualizar para v{version}',
|
||||||
reloadClientVersion: 'Recarregar para v{version}',
|
reloadClientVersion: 'Recarregar para v{version}',
|
||||||
updating: 'Atualizando...',
|
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',
|
updateFailed: 'Falha na atualizacao',
|
||||||
logout: 'Sair',
|
logout: 'Sair',
|
||||||
nodeVersionWarning: 'Node.js v{version} detectado. Atualize para a versao 23 ou posterior.',
|
nodeVersionWarning: 'Node.js v{version} detectado. Atualize para a versao 23 ou posterior.',
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export default {
|
|||||||
updateVersion: '升級版本 v{version}',
|
updateVersion: '升級版本 v{version}',
|
||||||
reloadClientVersion: '重新整理到 v{version}',
|
reloadClientVersion: '重新整理到 v{version}',
|
||||||
updating: '正在更新...',
|
updating: '正在更新...',
|
||||||
updateSuccess: '更新完成,請重新啟動服務',
|
updateSuccess: '更新成功,請稍後重新整理頁面,如長時間未啟動,請手動啟動',
|
||||||
updateFailed: '更新失敗',
|
updateFailed: '更新失敗',
|
||||||
logout: '登出',
|
logout: '登出',
|
||||||
nodeVersionWarning: '偵測到 Node.js v{version},請升級至 23 以上版本。',
|
nodeVersionWarning: '偵測到 Node.js v{version},請升級至 23 以上版本。',
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export default {
|
|||||||
updateVersion: '升级版本 v{version}',
|
updateVersion: '升级版本 v{version}',
|
||||||
reloadClientVersion: '刷新到 v{version}',
|
reloadClientVersion: '刷新到 v{version}',
|
||||||
updating: '正在更新...',
|
updating: '正在更新...',
|
||||||
updateSuccess: '更新完成,请重启服务',
|
updateSuccess: '更新成功,请稍后刷新页面,如长时间未启动,请手动启动',
|
||||||
updateFailed: '更新失败',
|
updateFailed: '更新失败',
|
||||||
logout: '退出登录',
|
logout: '退出登录',
|
||||||
nodeVersionWarning: '检测到 Node.js v{version},请升级到23以上版本。',
|
nodeVersionWarning: '检测到 Node.js v{version},请升级到23以上版本。',
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { execFileSync, spawn } from 'child_process'
|
|||||||
import { existsSync } from 'fs'
|
import { existsSync } from 'fs'
|
||||||
import { delimiter, dirname, join } from 'path'
|
import { delimiter, dirname, join } from 'path'
|
||||||
|
|
||||||
|
let updateInProgress = false
|
||||||
|
|
||||||
function getNodeBinDir() {
|
function getNodeBinDir() {
|
||||||
return dirname(process.execPath)
|
return dirname(process.execPath)
|
||||||
}
|
}
|
||||||
@@ -10,27 +12,39 @@ function getNodePrefix() {
|
|||||||
return process.platform === 'win32' ? getNodeBinDir() : dirname(getNodeBinDir())
|
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 prefix = getNodePrefix()
|
||||||
const candidates = process.platform === 'win32'
|
const homebrewPrefix = getHomebrewPrefix()
|
||||||
|
|
||||||
|
return process.platform === 'win32'
|
||||||
? [
|
? [
|
||||||
join(prefix, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
join(prefix, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
||||||
join(getNodeBinDir(), '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)
|
join(prefix, 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
||||||
|
...(homebrewPrefix ? [join(homebrewPrefix, 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js')] : []),
|
||||||
if (!npmCli) {
|
]
|
||||||
throw new Error(`Unable to locate npm CLI for ${process.execPath}; checked ${candidates.join(', ')}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return npmCli
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGlobalPackageBin(prefix: string) {
|
function getNpmCliPath() {
|
||||||
return process.platform === 'win32'
|
const candidates = getNpmCliCandidates()
|
||||||
? join(prefix, 'node_modules', 'hermes-web-ui', 'bin', 'hermes-web-ui.mjs')
|
const npmCli = candidates.find(existsSync)
|
||||||
: join(prefix, 'lib', 'node_modules', 'hermes-web-ui', 'bin', 'hermes-web-ui.mjs')
|
|
||||||
|
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() {
|
function getCurrentNodeEnv() {
|
||||||
@@ -42,7 +56,11 @@ function getCurrentNodeEnv() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function runNpm(args: string[], options: { timeout?: number } = {}) {
|
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',
|
encoding: 'utf-8',
|
||||||
timeout: options.timeout,
|
timeout: options.timeout,
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
@@ -50,12 +68,16 @@ function runNpm(args: string[], options: { timeout?: number } = {}) {
|
|||||||
}).trim()
|
}).trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGlobalPrefix() {
|
function getGlobalRoot() {
|
||||||
return runNpm(['prefix', '-g'])
|
return runNpm(['root', '-g'])
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGlobalCliScript() {
|
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() {
|
function runUpdateInstall() {
|
||||||
@@ -74,6 +96,17 @@ function spawnRestart(port: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function handleUpdate(ctx: any) {
|
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 {
|
try {
|
||||||
const output = runUpdateInstall()
|
const output = runUpdateInstall()
|
||||||
|
|
||||||
@@ -83,13 +116,27 @@ export async function handleUpdate(ctx: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
let restart
|
||||||
try {
|
try {
|
||||||
spawnRestart(process.env.PORT || '8648').unref()
|
restart = spawnRestart(process.env.PORT || '8648')
|
||||||
} finally {
|
} catch (err) {
|
||||||
process.exit(0)
|
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)
|
}, 3000)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
updateInProgress = false
|
||||||
ctx.status = 500
|
ctx.status = 500
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user