fix(stage-tamagotchi): websocket started duplicated-ly
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import type { ElectronServerChannelTlsConfig } from '../../../../shared/eventa'
|
||||
|
||||
import { X509Certificate } from 'node:crypto'
|
||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { isIP } from 'node:net'
|
||||
@@ -11,11 +13,79 @@ import { createContext } from '@moeru/eventa/adapters/electron/main'
|
||||
import { app, ipcMain } from 'electron'
|
||||
import { createCA, createCert } from 'mkcert'
|
||||
import { x } from 'tinyexec'
|
||||
import { nullable, object, record, string, unknown } from 'valibot'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { electronRestartWebSocketServer, electronStartWebSocketServer } from '../../../../shared/eventa'
|
||||
import { onAppBeforeQuit } from '../../../libs/bootkit/lifecycle'
|
||||
import {
|
||||
electronApplyServerChannelConfig,
|
||||
electronGetServerChannelConfig,
|
||||
electronRestartWebSocketServer,
|
||||
|
||||
electronStartWebSocketServer,
|
||||
} from '../../../../shared/eventa'
|
||||
import { onAppBeforeQuit, onAppReady } from '../../../libs/bootkit/lifecycle'
|
||||
import { createConfig } from '../../../libs/electron/persistence'
|
||||
|
||||
let serverInstance: { close: (closeActiveConnections?: boolean) => Promise<void> } | null = null
|
||||
let isServerQuitHookRegistered = false
|
||||
|
||||
const channelServerConfigSchema = object({
|
||||
websocketTlsConfig: nullable(record(string(), unknown())),
|
||||
})
|
||||
|
||||
const channelServerInvokeConfigSchema = z.object({
|
||||
websocketTlsConfig: z.record(z.string(), z.unknown()).nullable().optional(),
|
||||
}).strict()
|
||||
|
||||
const channelServerConfigStore = createConfig('server-channel', 'config.json', channelServerConfigSchema, {
|
||||
default: {
|
||||
websocketTlsConfig: null,
|
||||
},
|
||||
autoHeal: true,
|
||||
})
|
||||
|
||||
function getChannelServerConfig() {
|
||||
return channelServerConfigStore.get() ?? { websocketTlsConfig: null }
|
||||
}
|
||||
|
||||
function normalizeChannelServerOptions(
|
||||
payload: unknown,
|
||||
fallback = getChannelServerConfig(),
|
||||
) {
|
||||
const parsed = channelServerInvokeConfigSchema.safeParse(payload)
|
||||
if (!parsed.success) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return {
|
||||
websocketTlsConfig: parsed.data.websocketTlsConfig ?? fallback.websocketTlsConfig,
|
||||
}
|
||||
}
|
||||
|
||||
function registerServerQuitHook() {
|
||||
if (isServerQuitHookRegistered)
|
||||
return
|
||||
|
||||
isServerQuitHookRegistered = true
|
||||
|
||||
onAppBeforeQuit(async () => {
|
||||
const log = useLogg('main/server-runtime').useGlobalConfig()
|
||||
if (serverInstance && typeof serverInstance.close === 'function') {
|
||||
try {
|
||||
await serverInstance.close()
|
||||
log.log('WebSocket server closed')
|
||||
}
|
||||
catch (error) {
|
||||
const nodejsError = error as NodeJS.ErrnoException
|
||||
if ('code' in nodejsError && nodejsError.code === 'ERR_SERVER_NOT_RUNNING') {
|
||||
return
|
||||
}
|
||||
|
||||
log.withError(error).error('Error closing WebSocket server')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getLocalIPs(): string[] {
|
||||
const interfaces = networkInterfaces()
|
||||
@@ -190,10 +260,10 @@ async function getOrCreateCertificate() {
|
||||
return { cert, key }
|
||||
}
|
||||
|
||||
export async function setupServerChannel(options?: { websocketSecureEnabled?: boolean }) {
|
||||
export async function setupServerChannel(options?: { websocketTlsConfig?: ElectronServerChannelTlsConfig | null }) {
|
||||
const log = useLogg('main/server-runtime').useGlobalConfig()
|
||||
|
||||
const secureEnabled = options?.websocketSecureEnabled ?? false
|
||||
const secureEnabled = options?.websocketTlsConfig != null
|
||||
|
||||
try {
|
||||
const serverRuntime = await import('@proj-airi/server-runtime')
|
||||
@@ -255,30 +325,13 @@ export async function setupServerChannel(options?: { websocketSecureEnabled?: bo
|
||||
else {
|
||||
log.log(`@proj-airi/server-runtime started on ${protocol}://${hostname}:${port}`)
|
||||
}
|
||||
|
||||
onAppBeforeQuit(async () => {
|
||||
if (serverInstance && typeof serverInstance.close === 'function') {
|
||||
try {
|
||||
await serverInstance.close()
|
||||
log.log('WebSocket server closed')
|
||||
}
|
||||
catch (error) {
|
||||
const nodejsError = error as NodeJS.ErrnoException
|
||||
if ('code' in nodejsError && nodejsError.code === 'ERR_SERVER_NOT_RUNNING') {
|
||||
return
|
||||
}
|
||||
|
||||
log.withError(error).error('Error closing WebSocket server')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
log.withError(error).error('failed to start WebSocket server')
|
||||
}
|
||||
}
|
||||
|
||||
export async function restartServerChannel(options?: { websocketSecureEnabled?: boolean }) {
|
||||
export async function restartServerChannel(options?: { websocketTlsConfig?: ElectronServerChannelTlsConfig | null }) {
|
||||
const log = useLogg('main/server-runtime').useGlobalConfig()
|
||||
log.log('restarting server channel', { options })
|
||||
|
||||
@@ -299,12 +352,44 @@ export async function restartServerChannel(options?: { websocketSecureEnabled?:
|
||||
|
||||
export function setupServerChannelHandlers() {
|
||||
const { context } = createContext(ipcMain)
|
||||
channelServerConfigStore.setup()
|
||||
registerServerQuitHook()
|
||||
|
||||
defineInvokeHandler(context, electronGetServerChannelConfig, async () => {
|
||||
return getChannelServerConfig()
|
||||
})
|
||||
|
||||
defineInvokeHandler(context, electronApplyServerChannelConfig, async (req) => {
|
||||
const current = getChannelServerConfig()
|
||||
const next = normalizeChannelServerOptions(req, current)
|
||||
const changed = JSON.stringify(next.websocketTlsConfig) !== JSON.stringify(current.websocketTlsConfig)
|
||||
|
||||
channelServerConfigStore.update(next)
|
||||
|
||||
if (changed) {
|
||||
await restartServerChannel(next)
|
||||
}
|
||||
else if (!serverInstance) {
|
||||
await setupServerChannel(next)
|
||||
}
|
||||
|
||||
return next
|
||||
})
|
||||
|
||||
defineInvokeHandler(context, electronStartWebSocketServer, async (req) => {
|
||||
await setupServerChannel({ websocketSecureEnabled: req?.websocketSecureEnabled })
|
||||
const options = normalizeChannelServerOptions(req)
|
||||
await setupServerChannel(options)
|
||||
})
|
||||
|
||||
defineInvokeHandler(context, electronRestartWebSocketServer, async (req) => {
|
||||
await restartServerChannel({ websocketSecureEnabled: req?.websocketSecureEnabled })
|
||||
const current = getChannelServerConfig()
|
||||
const options = normalizeChannelServerOptions(req, current)
|
||||
channelServerConfigStore.update(options)
|
||||
await restartServerChannel(options)
|
||||
})
|
||||
|
||||
onAppReady(async () => {
|
||||
const options = getChannelServerConfig()
|
||||
await setupServerChannel(options)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import { toast, Toaster } from 'vue-sonner'
|
||||
import ResizeHandler from './components/ResizeHandler.vue'
|
||||
|
||||
import {
|
||||
electronGetServerChannelConfig,
|
||||
electronOpenSettings,
|
||||
electronPluginInspect,
|
||||
electronPluginList,
|
||||
@@ -33,11 +34,11 @@ import {
|
||||
electronPluginUnload,
|
||||
electronPluginUpdateCapability,
|
||||
electronStartTrackMousePosition,
|
||||
electronStartWebSocketServer,
|
||||
pluginProtocolListProviders,
|
||||
pluginProtocolListProvidersEventName,
|
||||
} from '../shared/eventa'
|
||||
import { useElectronEventaContext, useElectronEventaInvoke } from './composables/electron-vueuse'
|
||||
import { useServerChannelSettingsStore } from './stores/settings/server-channel'
|
||||
|
||||
const { isDark: dark } = useTheme()
|
||||
const i18n = useI18n()
|
||||
@@ -45,6 +46,7 @@ const contextBridgeStore = useContextBridgeStore()
|
||||
const displayModelsStore = useDisplayModelsStore()
|
||||
const settingsStore = useSettings()
|
||||
const { language, themeColorsHue, themeColorsHueDynamic } = storeToRefs(settingsStore)
|
||||
const serverChannelSettingsStore = useServerChannelSettingsStore()
|
||||
const onboardingStore = useOnboardingStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -65,10 +67,9 @@ watch(dark, () => updateThemeColor(), { immediate: true })
|
||||
watch(route, () => updateThemeColor(), { immediate: true })
|
||||
onMounted(() => updateThemeColor())
|
||||
|
||||
const startWebSocketServer = useElectronEventaInvoke(electronStartWebSocketServer)
|
||||
|
||||
onMounted(async () => {
|
||||
const context = useElectronEventaContext()
|
||||
const getServerChannelConfig = useElectronEventaInvoke(electronGetServerChannelConfig)
|
||||
const listPlugins = useElectronEventaInvoke(electronPluginList)
|
||||
const setPluginEnabled = useElectronEventaInvoke(electronPluginSetEnabled)
|
||||
const loadEnabledPlugins = useElectronEventaInvoke(electronPluginLoadEnabled)
|
||||
@@ -93,7 +94,9 @@ onMounted(async () => {
|
||||
await chatSessionStore.initialize()
|
||||
await displayModelsStore.loadDisplayModelsFromIndexedDB()
|
||||
await settingsStore.initializeStageModel()
|
||||
await startWebSocketServer({ websocketSecureEnabled: settingsStore.websocketSecureEnabled })
|
||||
|
||||
const serverChannelConfig = await getServerChannelConfig()
|
||||
serverChannelSettingsStore.websocketTlsConfig = serverChannelConfig.websocketTlsConfig
|
||||
|
||||
await serverChannelStore.initialize({ possibleEvents: ['ui:configure'] }).catch(err => console.error('Failed to initialize Mods Server Channel in App.vue:', err))
|
||||
await contextBridgeStore.initialize()
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import SettingsGeneralFields from '@proj-airi/stage-pages/components/settings-general-fields.vue'
|
||||
|
||||
import { useSettings } from '@proj-airi/stage-ui/stores/settings'
|
||||
import { FieldCheckbox } from '@proj-airi/ui'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { electronRestartWebSocketServer } from '../../../../shared/eventa'
|
||||
import { electronApplyServerChannelConfig } from '../../../../shared/eventa'
|
||||
import { useElectronEventaInvoke } from '../../../composables/electron-vueuse'
|
||||
import { useServerChannelSettingsStore } from '../../../stores/settings/server-channel'
|
||||
|
||||
const settings = useSettings()
|
||||
const serverChannelSettingsStore = useServerChannelSettingsStore()
|
||||
const { websocketTlsConfig } = storeToRefs(serverChannelSettingsStore)
|
||||
const { t } = useI18n()
|
||||
|
||||
const restartServer = useElectronEventaInvoke(electronRestartWebSocketServer)
|
||||
const applyServerChannelConfig = useElectronEventaInvoke(electronApplyServerChannelConfig)
|
||||
|
||||
watch(() => settings.websocketSecureEnabled, async (newValue) => {
|
||||
await restartServer({ websocketSecureEnabled: newValue })
|
||||
function setWebSocketTlsEnabled(value: boolean) {
|
||||
websocketTlsConfig.value = value ? {} : null
|
||||
}
|
||||
|
||||
watch(() => websocketTlsConfig.value != null, async (newValue) => {
|
||||
await applyServerChannelConfig({ websocketTlsConfig: newValue ? {} : null })
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -23,14 +29,15 @@ watch(() => settings.websocketSecureEnabled, async (newValue) => {
|
||||
<SettingsGeneralFields>
|
||||
<template #additional-fields>
|
||||
<FieldCheckbox
|
||||
v-model="settings.websocketSecureEnabled"
|
||||
v-motion
|
||||
:model-value="websocketTlsConfig != null"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:enter="{ opacity: 1, y: 0 }"
|
||||
:duration="250 + (5 * 10)"
|
||||
:delay="5 * 50"
|
||||
:label="t('settings.websocket-secure-enabled.title')"
|
||||
:description="t('settings.websocket-secure-enabled.description')"
|
||||
@update:model-value="setWebSocketTlsEnabled"
|
||||
/>
|
||||
</template>
|
||||
</SettingsGeneralFields>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { ElectronServerChannelTlsConfig } from '../../../shared/eventa'
|
||||
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useServerChannelSettingsStore = defineStore('tamagotchi-server-channel-settings', () => {
|
||||
const websocketTlsConfig = useLocalStorage<ElectronServerChannelTlsConfig | null>('settings/server-channel/websocket-tls-config', null)
|
||||
|
||||
return {
|
||||
websocketTlsConfig,
|
||||
}
|
||||
})
|
||||
@@ -2,13 +2,24 @@ import { defineEventa, defineInvokeEventa } from '@moeru/eventa'
|
||||
|
||||
export const electronStartTrackMousePosition = defineInvokeEventa('eventa:invoke:electron:start-tracking-mouse-position')
|
||||
export const electronStartDraggingWindow = defineInvokeEventa('eventa:invoke:electron:start-dragging-window')
|
||||
|
||||
export const electronOpenMainDevtools = defineInvokeEventa('eventa:invoke:electron:windows:main:devtools:open')
|
||||
export const electronOpenSettings = defineInvokeEventa('eventa:invoke:electron:windows:settings:open')
|
||||
export const electronOpenChat = defineInvokeEventa('eventa:invoke:electron:windows:chat:open')
|
||||
export const electronOpenSettingsDevtools = defineInvokeEventa('eventa:invoke:electron:windows:settings:devtools:open')
|
||||
export const electronOpenDevtoolsWindow = defineInvokeEventa<void, { route?: string }>('eventa:invoke:electron:windows:devtools:open')
|
||||
export const electronStartWebSocketServer = defineInvokeEventa<void, { websocketSecureEnabled: boolean }>('eventa:invoke:electron:start-websocket-server')
|
||||
export const electronRestartWebSocketServer = defineInvokeEventa<void, { websocketSecureEnabled: boolean }>('eventa:invoke:electron:restart-websocket-server')
|
||||
|
||||
export interface ElectronServerChannelTlsConfig {
|
||||
[key: string]: unknown
|
||||
}
|
||||
export const electronStartWebSocketServer = defineInvokeEventa<void, { websocketTlsConfig: ElectronServerChannelTlsConfig | null }>('eventa:invoke:electron:start-websocket-server')
|
||||
export const electronRestartWebSocketServer = defineInvokeEventa<void, { websocketTlsConfig: ElectronServerChannelTlsConfig | null }>('eventa:invoke:electron:restart-websocket-server')
|
||||
export interface ElectronServerChannelConfig {
|
||||
websocketTlsConfig: ElectronServerChannelTlsConfig | null
|
||||
}
|
||||
export const electronGetServerChannelConfig = defineInvokeEventa<ElectronServerChannelConfig>('eventa:invoke:electron:server-channel:get-config')
|
||||
export const electronApplyServerChannelConfig = defineInvokeEventa<ElectronServerChannelConfig, Partial<ElectronServerChannelConfig>>('eventa:invoke:electron:server-channel:apply-config')
|
||||
|
||||
export const electronPluginList = defineInvokeEventa<PluginRegistrySnapshot>('eventa:invoke:electron:plugins:list')
|
||||
export const electronPluginSetEnabled = defineInvokeEventa<PluginRegistrySnapshot, { name: string, enabled: boolean, path?: string }>('eventa:invoke:electron:plugins:set-enabled')
|
||||
export const electronPluginLoadEnabled = defineInvokeEventa<PluginRegistrySnapshot>('eventa:invoke:electron:plugins:load-enabled')
|
||||
@@ -16,8 +27,10 @@ export const electronPluginLoad = defineInvokeEventa<PluginRegistrySnapshot, { n
|
||||
export const electronPluginUnload = defineInvokeEventa<PluginRegistrySnapshot, { name: string }>('eventa:invoke:electron:plugins:unload')
|
||||
export const electronPluginInspect = defineInvokeEventa<PluginHostDebugSnapshot>('eventa:invoke:electron:plugins:inspect')
|
||||
export const electronPluginUpdateCapability = defineInvokeEventa<PluginCapabilityState, PluginCapabilityPayload>('eventa:invoke:electron:plugins:capability:update')
|
||||
|
||||
export const pluginProtocolListProvidersEventName = 'proj-airi:plugin-sdk:apis:protocol:resources:providers:list-providers'
|
||||
export const pluginProtocolListProviders = defineInvokeEventa<Array<{ name: string }>>(pluginProtocolListProvidersEventName)
|
||||
|
||||
export const captionIsFollowingWindowChanged = defineEventa<boolean>('eventa:event:electron:windows:caption-overlay:is-following-window-changed')
|
||||
export const captionGetIsFollowingWindow = defineInvokeEventa<boolean>('eventa:invoke:electron:windows:caption-overlay:get-is-following-window')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user