fix profile runtime status loading (#1142)
This commit is contained in:
@@ -52,6 +52,11 @@ export interface ProfileRuntimeStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProfileRuntimeStatusesResponse {
|
||||||
|
profiles: ProfileRuntimeStatus[]
|
||||||
|
refreshing?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchProfiles(): Promise<HermesProfile[]> {
|
export async function fetchProfiles(): Promise<HermesProfile[]> {
|
||||||
const res = await request<{ profiles: HermesProfile[] }>('/api/hermes/profiles')
|
const res = await request<{ profiles: HermesProfile[] }>('/api/hermes/profiles')
|
||||||
return res.profiles
|
return res.profiles
|
||||||
@@ -66,8 +71,13 @@ export async function fetchProfileRuntimeStatus(name: string): Promise<ProfileRu
|
|||||||
return request<ProfileRuntimeStatus>(`/api/hermes/profiles/${encodeURIComponent(name)}/runtime-status`)
|
return request<ProfileRuntimeStatus>(`/api/hermes/profiles/${encodeURIComponent(name)}/runtime-status`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchProfileRuntimeStatusesWithMeta(options: { refresh?: boolean } = {}): Promise<ProfileRuntimeStatusesResponse> {
|
||||||
|
const query = options.refresh === false ? '?refresh=0' : ''
|
||||||
|
return request<ProfileRuntimeStatusesResponse>(`/api/hermes/profiles/runtime-statuses${query}`)
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchProfileRuntimeStatuses(): Promise<ProfileRuntimeStatus[]> {
|
export async function fetchProfileRuntimeStatuses(): Promise<ProfileRuntimeStatus[]> {
|
||||||
const res = await request<{ profiles: ProfileRuntimeStatus[] }>('/api/hermes/profiles/runtime-statuses')
|
const res = await fetchProfileRuntimeStatusesWithMeta()
|
||||||
return res.profiles
|
return res.profiles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { computed, onMounted, ref } from 'vue'
|
|||||||
import { NButton, NModal, NSpin, useMessage } from 'naive-ui'
|
import { NButton, NModal, NSpin, useMessage } from 'naive-ui'
|
||||||
import { useProfilesStore } from '@/stores/hermes/profiles'
|
import { useProfilesStore } from '@/stores/hermes/profiles'
|
||||||
import {
|
import {
|
||||||
fetchProfileRuntimeStatuses,
|
fetchProfileRuntimeStatusesWithMeta,
|
||||||
restartProfileGateway,
|
restartProfileGateway,
|
||||||
restartProfileRuntime,
|
restartProfileRuntime,
|
||||||
type HermesProfile,
|
type HermesProfile,
|
||||||
@@ -31,21 +31,43 @@ const gatewayRestarting = ref<Record<string, boolean>>({})
|
|||||||
const profileRestarting = ref<Record<string, boolean>>({})
|
const profileRestarting = ref<Record<string, boolean>>({})
|
||||||
const profileSwitching = ref<Record<string, boolean>>({})
|
const profileSwitching = ref<Record<string, boolean>>({})
|
||||||
const statusByProfile = computed(() => new Map(runtimeStatuses.value.map(status => [status.profile, status])))
|
const statusByProfile = computed(() => new Map(runtimeStatuses.value.map(status => [status.profile, status])))
|
||||||
|
let runtimeRefreshToken = 0
|
||||||
|
|
||||||
async function loadRuntimeStatuses() {
|
async function loadRuntimeStatuses(options: { background?: boolean } = {}): Promise<boolean> {
|
||||||
runtimeLoading.value = true
|
const token = ++runtimeRefreshToken
|
||||||
|
if (!options.background) {
|
||||||
|
runtimeLoading.value = runtimeStatuses.value.length === 0
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
runtimeStatuses.value = await fetchProfileRuntimeStatuses()
|
const res = await fetchProfileRuntimeStatusesWithMeta({ refresh: !options.background })
|
||||||
|
if (token !== runtimeRefreshToken) return false
|
||||||
|
runtimeStatuses.value = res.profiles
|
||||||
|
return !!res.refreshing
|
||||||
} catch {
|
} catch {
|
||||||
runtimeStatuses.value = []
|
runtimeStatuses.value = []
|
||||||
|
return false
|
||||||
} finally {
|
} finally {
|
||||||
|
if (token === runtimeRefreshToken) {
|
||||||
runtimeLoading.value = false
|
runtimeLoading.value = false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openProfileModal() {
|
function openProfileModal() {
|
||||||
showProfileModal.value = true
|
showProfileModal.value = true
|
||||||
void loadRuntimeStatuses()
|
void loadRuntimeStatuses().then((refreshing) => {
|
||||||
|
if (refreshing) scheduleRuntimeStatusPoll()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleRuntimeStatusPoll(attempt = 0) {
|
||||||
|
if (attempt >= 12 || typeof window === 'undefined') return
|
||||||
|
window.setTimeout(() => {
|
||||||
|
if (!showProfileModal.value) return
|
||||||
|
void loadRuntimeStatuses({ background: true }).then((refreshing) => {
|
||||||
|
if (refreshing) scheduleRuntimeStatusPoll(attempt + 1)
|
||||||
|
})
|
||||||
|
}, attempt === 0 ? 700 : 1200)
|
||||||
}
|
}
|
||||||
|
|
||||||
function openAvatarModal(profile: HermesProfile) {
|
function openAvatarModal(profile: HermesProfile) {
|
||||||
@@ -116,10 +138,12 @@ async function handleAvatarFileChange(event: Event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function gatewayStatusText(running?: boolean) {
|
function gatewayStatusText(running?: boolean) {
|
||||||
|
if (running == null) return t('profiles.runtime.checking')
|
||||||
return running ? t('profiles.runtime.running') : t('profiles.runtime.stopped')
|
return running ? t('profiles.runtime.running') : t('profiles.runtime.stopped')
|
||||||
}
|
}
|
||||||
|
|
||||||
function bridgeStatusText(running?: boolean) {
|
function bridgeStatusText(running?: boolean) {
|
||||||
|
if (running == null) return t('profiles.runtime.checking')
|
||||||
return running ? t('profiles.runtime.active') : t('profiles.runtime.idle')
|
return running ? t('profiles.runtime.active') : t('profiles.runtime.idle')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -706,6 +706,7 @@ jobTriggered: 'Job ausgelost',
|
|||||||
active: 'Aktiv',
|
active: 'Aktiv',
|
||||||
activeTag: 'Aktuell',
|
activeTag: 'Aktuell',
|
||||||
idle: 'Leerlauf',
|
idle: 'Leerlauf',
|
||||||
|
checking: 'Prüfung läuft',
|
||||||
running: 'Läuft',
|
running: 'Läuft',
|
||||||
stopped: 'Gestoppt',
|
stopped: 'Gestoppt',
|
||||||
restartGateway: 'Gateway neu starten',
|
restartGateway: 'Gateway neu starten',
|
||||||
|
|||||||
@@ -809,6 +809,7 @@ export default {
|
|||||||
active: 'Active',
|
active: 'Active',
|
||||||
activeTag: 'Active',
|
activeTag: 'Active',
|
||||||
idle: 'Idle',
|
idle: 'Idle',
|
||||||
|
checking: 'Checking',
|
||||||
running: 'Running',
|
running: 'Running',
|
||||||
stopped: 'Stopped',
|
stopped: 'Stopped',
|
||||||
restartGateway: 'Restart Gateway',
|
restartGateway: 'Restart Gateway',
|
||||||
|
|||||||
@@ -706,6 +706,7 @@ jobTriggered: 'Job ejecutado',
|
|||||||
active: 'Activo',
|
active: 'Activo',
|
||||||
activeTag: 'Actual',
|
activeTag: 'Actual',
|
||||||
idle: 'Inactivo',
|
idle: 'Inactivo',
|
||||||
|
checking: 'Comprobando',
|
||||||
running: 'En ejecución',
|
running: 'En ejecución',
|
||||||
stopped: 'Detenido',
|
stopped: 'Detenido',
|
||||||
restartGateway: 'Reiniciar gateway',
|
restartGateway: 'Reiniciar gateway',
|
||||||
|
|||||||
@@ -706,6 +706,7 @@ jobTriggered: 'Job declenche',
|
|||||||
active: 'Actif',
|
active: 'Actif',
|
||||||
activeTag: 'Actuel',
|
activeTag: 'Actuel',
|
||||||
idle: 'Inactif',
|
idle: 'Inactif',
|
||||||
|
checking: 'Vérification',
|
||||||
running: 'En cours',
|
running: 'En cours',
|
||||||
stopped: 'Arrêté',
|
stopped: 'Arrêté',
|
||||||
restartGateway: 'Redémarrer le gateway',
|
restartGateway: 'Redémarrer le gateway',
|
||||||
|
|||||||
@@ -706,6 +706,7 @@ export default {
|
|||||||
active: 'アクティブ',
|
active: 'アクティブ',
|
||||||
activeTag: '現在',
|
activeTag: '現在',
|
||||||
idle: '待機中',
|
idle: '待機中',
|
||||||
|
checking: '確認中',
|
||||||
running: '実行中',
|
running: '実行中',
|
||||||
stopped: '停止中',
|
stopped: '停止中',
|
||||||
restartGateway: 'Gateway を再起動',
|
restartGateway: 'Gateway を再起動',
|
||||||
|
|||||||
@@ -706,6 +706,7 @@ export default {
|
|||||||
active: '활성',
|
active: '활성',
|
||||||
activeTag: '현재',
|
activeTag: '현재',
|
||||||
idle: '대기 중',
|
idle: '대기 중',
|
||||||
|
checking: '확인 중',
|
||||||
running: '실행 중',
|
running: '실행 중',
|
||||||
stopped: '중지됨',
|
stopped: '중지됨',
|
||||||
restartGateway: 'Gateway 재시작',
|
restartGateway: 'Gateway 재시작',
|
||||||
|
|||||||
@@ -706,6 +706,7 @@ jobTriggered: 'Job acionado',
|
|||||||
active: 'Ativo',
|
active: 'Ativo',
|
||||||
activeTag: 'Atual',
|
activeTag: 'Atual',
|
||||||
idle: 'Ocioso',
|
idle: 'Ocioso',
|
||||||
|
checking: 'Verificando',
|
||||||
running: 'Em execução',
|
running: 'Em execução',
|
||||||
stopped: 'Parado',
|
stopped: 'Parado',
|
||||||
restartGateway: 'Reiniciar gateway',
|
restartGateway: 'Reiniciar gateway',
|
||||||
|
|||||||
@@ -801,6 +801,7 @@ export default {
|
|||||||
active: '使用中',
|
active: '使用中',
|
||||||
activeTag: '目前',
|
activeTag: '目前',
|
||||||
idle: '閒置',
|
idle: '閒置',
|
||||||
|
checking: '檢測中',
|
||||||
running: '執行中',
|
running: '執行中',
|
||||||
stopped: '已停止',
|
stopped: '已停止',
|
||||||
restartGateway: '重啟閘道',
|
restartGateway: '重啟閘道',
|
||||||
|
|||||||
@@ -801,6 +801,7 @@ export default {
|
|||||||
active: '活跃',
|
active: '活跃',
|
||||||
activeTag: '当前',
|
activeTag: '当前',
|
||||||
idle: '空闲',
|
idle: '空闲',
|
||||||
|
checking: '检测中',
|
||||||
running: '运行中',
|
running: '运行中',
|
||||||
stopped: '已停止',
|
stopped: '已停止',
|
||||||
restartGateway: '重启网关',
|
restartGateway: '重启网关',
|
||||||
|
|||||||
@@ -35,6 +35,17 @@ interface ProfileAvatarResponse {
|
|||||||
updatedAt?: number
|
updatedAt?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RuntimeStatus = Awaited<ReturnType<typeof buildRuntimeStatus>>
|
||||||
|
|
||||||
|
interface RuntimeStatusCacheEntry {
|
||||||
|
status: RuntimeStatus
|
||||||
|
updatedAt: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const runtimeStatusCache = new Map<string, RuntimeStatusCacheEntry>()
|
||||||
|
let runtimeStatusRefreshPromise: Promise<void> | null = null
|
||||||
|
let runtimeStatusMinimumFreshAt = 0
|
||||||
|
|
||||||
const RESERVED_PROFILE_NAMES = new Set([
|
const RESERVED_PROFILE_NAMES = new Set([
|
||||||
'hermes', 'default', 'test', 'tmp', 'root', 'sudo',
|
'hermes', 'default', 'test', 'tmp', 'root', 'sudo',
|
||||||
])
|
])
|
||||||
@@ -309,6 +320,44 @@ async function buildRuntimeStatus(profile: HermesProfile | string, bridgeState?:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setRuntimeStatusCache(status: RuntimeStatus, checkedAt = Date.now()): void {
|
||||||
|
runtimeStatusCache.set(status.profile, {
|
||||||
|
status,
|
||||||
|
updatedAt: checkedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function listProfilesForStatusFast(): HermesProfile[] {
|
||||||
|
return filterVisibleProfiles(listProfilesFromDisk(getActiveProfileName()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshRuntimeStatusCache(checkedAt: number): Promise<void> {
|
||||||
|
const profiles = await listProfilesForStatus()
|
||||||
|
const bridge = await readBridgeWorkers()
|
||||||
|
const statuses = await Promise.all(profiles.map(profile => buildRuntimeStatus(profile, bridge)))
|
||||||
|
statuses.forEach(status => setRuntimeStatusCache(status, checkedAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
function startRuntimeStatusRefresh(): void {
|
||||||
|
const startedAt = Date.now()
|
||||||
|
runtimeStatusRefreshPromise = refreshRuntimeStatusCache(startedAt)
|
||||||
|
.catch((err) => {
|
||||||
|
logger.warn(err, '[profiles] failed to refresh runtime status cache')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
runtimeStatusRefreshPromise = null
|
||||||
|
if (runtimeStatusMinimumFreshAt > startedAt) {
|
||||||
|
startRuntimeStatusRefresh()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleRuntimeStatusRefresh(): void {
|
||||||
|
runtimeStatusMinimumFreshAt = Math.max(runtimeStatusMinimumFreshAt, Date.now())
|
||||||
|
if (runtimeStatusRefreshPromise) return
|
||||||
|
startRuntimeStatusRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
export async function list(ctx: any) {
|
export async function list(ctx: any) {
|
||||||
try {
|
try {
|
||||||
let profiles: HermesProfile[]
|
let profiles: HermesProfile[]
|
||||||
@@ -478,18 +527,35 @@ export async function runtimeStatus(ctx: any) {
|
|||||||
try {
|
try {
|
||||||
const profiles = await listProfilesForStatus()
|
const profiles = await listProfilesForStatus()
|
||||||
const profile = profiles.find(item => item.name === name)
|
const profile = profiles.find(item => item.name === name)
|
||||||
ctx.body = await buildRuntimeStatus(profile || name)
|
const status = await buildRuntimeStatus(profile || name)
|
||||||
|
setRuntimeStatusCache(status)
|
||||||
|
ctx.body = status
|
||||||
} catch {
|
} catch {
|
||||||
ctx.body = await buildRuntimeStatus(name)
|
const status = await buildRuntimeStatus(name)
|
||||||
|
setRuntimeStatusCache(status)
|
||||||
|
ctx.body = status
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runtimeStatuses(ctx: any) {
|
export async function runtimeStatuses(ctx: any) {
|
||||||
try {
|
try {
|
||||||
const profiles = filterProfilesForUser(ctx, await listProfilesForStatus())
|
const refreshParam = ctx.query?.refresh
|
||||||
const bridge = await readBridgeWorkers()
|
const refreshRequested = refreshParam === undefined || (refreshParam !== '0' && refreshParam !== 'false')
|
||||||
const statuses = await Promise.all(profiles.map(profile => buildRuntimeStatus(profile, bridge)))
|
if (refreshRequested) scheduleRuntimeStatusRefresh()
|
||||||
ctx.body = { profiles: statuses }
|
|
||||||
|
const profiles = filterProfilesForUser(ctx, listProfilesForStatusFast())
|
||||||
|
const statuses: RuntimeStatus[] = []
|
||||||
|
profiles.forEach(profile => {
|
||||||
|
const cached = runtimeStatusCache.get(profile.name)
|
||||||
|
if (cached && cached.updatedAt >= runtimeStatusMinimumFreshAt) {
|
||||||
|
statuses.push(cached.status)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx.body = {
|
||||||
|
profiles: statuses,
|
||||||
|
refreshing: !!runtimeStatusRefreshPromise,
|
||||||
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
ctx.status = 500
|
ctx.status = 500
|
||||||
ctx.body = { error: err.message }
|
ctx.body = { error: err.message }
|
||||||
@@ -519,6 +585,17 @@ export async function restartGatewayForProfile(ctx: any) {
|
|||||||
try {
|
try {
|
||||||
const result = await bridgeCleanupClient().destroyProfile(name)
|
const result = await bridgeCleanupClient().destroyProfile(name)
|
||||||
logger.info('[profiles] destroyed bridge sessions after gateway restart profile=%s destroyed=%s', name, result.destroyed)
|
logger.info('[profiles] destroyed bridge sessions after gateway restart profile=%s destroyed=%s', name, result.destroyed)
|
||||||
|
const cached = runtimeStatusCache.get(name)?.status
|
||||||
|
if (cached) {
|
||||||
|
setRuntimeStatusCache({
|
||||||
|
...cached,
|
||||||
|
bridge: {
|
||||||
|
...cached.bridge,
|
||||||
|
running: false,
|
||||||
|
},
|
||||||
|
gateway,
|
||||||
|
})
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.warn(err, '[profiles] failed to destroy bridge sessions after gateway restart profile=%s', name)
|
logger.warn(err, '[profiles] failed to destroy bridge sessions after gateway restart profile=%s', name)
|
||||||
}
|
}
|
||||||
@@ -542,10 +619,12 @@ export async function restartProfileRuntime(ctx: any) {
|
|||||||
logger.info('[profiles] destroyed bridge sessions after profile restart profile=%s destroyed=%s', name, result.destroyed)
|
logger.info('[profiles] destroyed bridge sessions after profile restart profile=%s destroyed=%s', name, result.destroyed)
|
||||||
const profiles = await listProfilesForStatus()
|
const profiles = await listProfilesForStatus()
|
||||||
const profile = profiles.find(item => item.name === name)
|
const profile = profiles.find(item => item.name === name)
|
||||||
|
const status = await buildRuntimeStatus(profile || name)
|
||||||
|
setRuntimeStatusCache(status)
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
success: true,
|
success: true,
|
||||||
destroyed: result.destroyed,
|
destroyed: result.destroyed,
|
||||||
status: await buildRuntimeStatus(profile || name),
|
status,
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
ctx.status = 500
|
ctx.status = 500
|
||||||
|
|||||||
Reference in New Issue
Block a user