Files
Starlight_Lancher/apps/app-frontend/src/pages/instance/Logs.vue
T
Disy ce0f41c973
Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
Sync LobeHub models / sync (push) Has been cancelled
fix: 修复实时日志与数据库切换可靠性
2026-09-27 14:18:25 +08:00

247 lines
5.8 KiB
Vue

<template>
<div class="flex flex-col gap-4 h-full">
<ConsolePageLayout />
</div>
</template>
<script setup>
import {
ConsolePageLayout,
defineMessages,
injectNotificationManager,
provideConsoleManager,
useVIntl,
} from '@modrinth/ui'
import { computed, onUnmounted, ref, shallowRef, triggerRef, watch, watchEffect } from 'vue'
import { useRoute } from 'vue-router'
import { useCrashAnalysis } from '@/composables/useCrashAnalysis'
import { useInstanceConsole } from '@/composables/useInstanceConsole'
import { log_listener, process_listener } from '@/helpers/events.js'
import {
delete_logs_by_filename,
export_crash_context,
get_output_by_filename,
} from '@/helpers/logs.js'
const { handleError } = injectNotificationManager()
const route = useRoute()
const { formatMessage } = useVIntl()
const messages = defineMessages({
liveLog: { id: 'instance.logs.source.live', defaultMessage: 'Live Log' },
unknownLog: { id: 'instance.logs.source.unknown', defaultMessage: 'Unknown' },
logName: { id: 'instance.logs.source.numbered', defaultMessage: 'Log {index}' },
cannotDeleteLatest: {
id: 'instance.logs.delete.latest-running',
defaultMessage: 'Cannot delete latest.log while the instance is running',
},
})
const props = defineProps({
instance: {
type: Object,
default() {
return {}
},
},
options: {
type: Object,
default() {
return {}
},
},
offline: {
type: Boolean,
default() {
return false
},
},
playing: {
type: Boolean,
default() {
return false
},
},
installed: {
type: Boolean,
default() {
return false
},
},
})
const instanceId = computed(() => route.params.id)
const {
analysis: localCrashAnalysis,
loading: crashAnalysisLoading,
refresh: refreshCrashAnalysis,
clear: clearCrashAnalysis,
} = useCrashAnalysis(instanceId.value)
const {
liveConsole,
historicalConsole,
hydrate,
getHistoricalLogs,
getHistoricalContent,
invalidate,
clearLive,
appendLive,
resyncLive,
} = useInstanceConsole(instanceId.value)
const unlistenLog = await log_listener((payload) => {
if (payload.instance_id !== instanceId.value) return
void appendLive(payload)?.catch(console.warn)
})
await hydrate().catch(handleError)
function buildLogList(rawLogs) {
return [
{ name: formatMessage(messages.liveLog), live: true },
...rawLogs
.filter(
(log) =>
log.filename !== 'latest_stdout.log' &&
log.filename !== 'latest_stdout' &&
log.filename !== 'launcher_log.txt' &&
(log.output == null || log.output !== '') &&
(log.filename.includes('.log') || log.filename.endsWith('.txt')),
)
.map((log) => ({
...log,
name: log.filename || formatMessage(messages.unknownLog),
})),
]
}
const logs = ref(buildLogList([]))
void getHistoricalLogs()
.then((allLogs) => {
logs.value = buildLogList(allLogs)
})
.catch(handleError)
const selectedLogIndex = ref(0)
const isLive = computed(() => selectedLogIndex.value === 0)
const filteredLogs = computed(() =>
props.playing ? logs.value.filter((l) => l.live || l.name !== 'latest.log') : logs.value,
)
const logSources = computed(() =>
filteredLogs.value.map((l, i) => ({
id: String(i),
name: l?.name ?? formatMessage(messages.logName, { index: i }),
live: l?.live ?? false,
})),
)
const activeConsole = computed(() => (isLive.value ? liveConsole : historicalConsole))
const logLines = shallowRef(activeConsole.value.output.value)
watchEffect(() => {
logLines.value = activeConsole.value.output.value
triggerRef(logLines)
})
async function analyseForCrash() {
await refreshCrashAnalysis().catch((error) => {
handleError(error)
})
}
async function exportCrashContext() {
await export_crash_context(props.instance.id, props.instance.name).catch(handleError)
}
const selectedLog = computed(() => filteredLogs.value[selectedLogIndex.value])
const deleteDisabled = computed(() => {
const log = selectedLog.value
if (!log || log.live) return true
return log.filename === 'latest.log' && props.playing
})
async function deleteSelectedLog() {
const log = selectedLog.value
if (!log || log.live) return
await delete_logs_by_filename(props.instance.id, log.log_type, log.filename)
invalidate()
const freshLogs = await getHistoricalLogs()
logs.value = buildLogList(freshLogs)
selectedLogIndex.value = 0
}
provideConsoleManager({
logLines,
logSources,
activeLogSourceIndex: selectedLogIndex,
showCommandInput: false,
loading: ref(false),
onClear: () => {
if (!isLive.value) return
void clearLive()
},
onDelete: deleteSelectedLog,
deleteDisabled,
deleteDisabledTooltip: computed(() => formatMessage(messages.cannotDeleteLatest)),
shareDisabled: computed(() => props.offline),
emptyStateType: 'instance',
localCrashAnalysis,
crashAnalysisLoading,
onExportCrashContext: exportCrashContext,
})
watch(selectedLogIndex, async (newIndex) => {
if (newIndex === 0) return
const log = filteredLogs.value[newIndex]
if (!log) return
const cached = getHistoricalContent(log.filename)
if (cached) {
historicalConsole.clear()
await historicalConsole.addLegacyLog(cached)
return
}
const output = await get_output_by_filename(props.instance.id, log.log_type, log.filename).catch(
handleError,
)
if (output) {
historicalConsole.clear()
await historicalConsole.addLegacyLog(output)
}
})
selectedLogIndex.value = 0
if (!props.playing) {
void analyseForCrash()
}
const unlistenProcesses = await process_listener(async (e) => {
if (e.instance_id !== instanceId.value) return
if (e.event === 'launched') {
await resyncLive().catch(console.warn)
clearCrashAnalysis()
invalidate()
selectedLogIndex.value = 0
}
if (e.event === 'finished') {
await resyncLive().catch(console.warn)
invalidate()
const freshLogs = await getHistoricalLogs()
logs.value = buildLogList(freshLogs)
void analyseForCrash()
}
})
onUnmounted(() => {
unlistenLog()
unlistenProcesses()
})
</script>