Files
Starlight_Lancher/apps/app-frontend/src/composables/useServerLifecycle.ts
T

167 lines
5.3 KiB
TypeScript

import { parseEula, setEulaAccepted } from '@modrinth/server'
import { injectNotificationManager } from '@modrinth/ui'
import { onScopeDispose, ref, useTemplateRef } from 'vue'
import type { ComponentExposed } from 'vue-component-type-helpers'
import type EulaModal from '@/components/multiplayer/servers/EulaModal.vue'
import { resumeModpackInstall } from '@/composables/useServerInstalls'
import { type ServerView, setServerExitReasonHandler, useServers } from '@/composables/useServers'
import { serverEventListener, servers as serversApi } from '@/helpers/servers'
import { injectDownloadManager } from '@/providers/download-manager'
/**
* Shared "start with EULA gate" flow used by the servers overview and the
* server detail page. Bind the returned `eulaModal` ref to an `<EulaModal>`
* rendered with `ref="eulaModal"` in the component template.
*/
export function useServerLifecycle() {
const { startServer } = useServers()
const { handleError } = injectNotificationManager()
// [SERVER-DOWNLOAD-BRIDGE] Capture the download manager once during Vue
// setup context. See the note in `startModpackServerInstall` for why
// this must be done here and not later.
let downloadManager: ReturnType<typeof injectDownloadManager> | null = null
try {
downloadManager = injectDownloadManager()
} catch {
// Not inside a provider tree — server downloads will not appear in sidebar.
}
const eulaModal = useTemplateRef<ComponentExposed<typeof EulaModal>>('eulaModal')
const eulaText = ref('')
let pendingId = ''
// Listen for real-time EULA prompt detection from the Rust backend
let unregisterEulaListener: (() => void) | null = null
serverEventListener((serverId, payload) => {
if (payload.event === 'eula_required' && payload.server_id === pendingId) {
eulaText.value = payload.eula_text
eulaModal.value?.show()
}
}).then((unregister) => {
unregisterEulaListener = unregister
})
onScopeDispose(() => {
unregisterEulaListener?.()
})
/**
* Starts the server, gating on the EULA file:
* - eula.txt missing → code-create it with `eula=false`, then show the modal
* - eula.txt present and `eula=false` → show the modal
* - eula.txt present and `eula=true` → start directly
*/
async function tryStartServer(server: ServerView) {
const accepted = await ensureEula(server.id)
if (accepted) {
await launchServer(server.id)
} else {
pendingId = server.id
eulaModal.value?.show()
}
}
/**
* Reads the server's eula.txt. When absent, writes a code-created
* `eula=false` file so the acceptance prompt can be shown without booting
* the server. Returns whether the EULA is already accepted.
*/
async function ensureEula(serverId: string): Promise<boolean> {
let text: string
try {
text = await serversApi.readFile(serverId, 'eula.txt')
} catch {
// eula.txt doesn't exist yet — code-create it with eula=false so the
// manual start gate can offer the prompt without starting the jar.
text = setEulaAccepted('', false)
await serversApi.writeFile(serverId, 'eula.txt', text).catch(() => {})
}
const doc = parseEula(text)
if (doc.accepted) return true
eulaText.value = text
return false
}
/**
* Starts the server and, when the start itself fails over an unaccepted
* EULA, falls back to the confirmation dialog instead of just the error.
*/
async function launchServer(serverId: string) {
const started = await startServer(serverId)
if (started) return
try {
const text = await serversApi.readFile(serverId, 'eula.txt')
if (parseEula(text).accepted) return
eulaText.value = text
pendingId = serverId
eulaModal.value?.show()
} catch {
// Start failed for a non-EULA reason; that error was already surfaced.
}
}
async function acceptEula() {
const id = pendingId
if (!id) return
try {
const updated = setEulaAccepted(eulaText.value, true)
await serversApi.writeFile(id, 'eula.txt', updated)
pendingId = ''
eulaModal.value?.hide()
// Start the server after accepting EULA
await startServer(id)
} catch (error) {
console.error(error)
}
}
function declineEula() {
pendingId = ''
eulaModal.value?.hide()
}
/**
* Offers the EULA dialog after the server exited on its own over an
* unaccepted EULA (detected from the process's final output). Accepting
* writes `eula.txt` and restarts, matching the pre-start gate.
*/
async function offerEulaAfterExit(serverId: string) {
try {
const text = await serversApi.readFile(serverId, 'eula.txt')
if (parseEula(text).accepted) return
eulaText.value = text
pendingId = serverId
eulaModal.value?.show()
} catch {
// No eula.txt to show; the exit stays unexplained.
}
}
const unregisterExitReasonHandler = setServerExitReasonHandler((serverId, reason) => {
if (reason === 'eula') void offerEulaAfterExit(serverId)
})
onScopeDispose(unregisterExitReasonHandler)
/** Resumes or retries an interrupted/failed modpack download for this server. */
async function resumeInstall(server: ServerView) {
try {
// [SERVER-DOWNLOAD-BRIDGE] Pass the download manager captured
// during setup so the synthetic job appears in sidebar.
await resumeModpackInstall(server, downloadManager)
} catch (error) {
handleError(error)
}
}
return {
eulaModal,
eulaText,
tryStartServer,
acceptEula,
declineEula,
resumeInstall,
offerEulaAfterExit,
}
}