feat: implement extension spawned process auto cleanup

If extension doesn't kill the processes it spawns, Kunkun will auto clean up all spawned processes on exit/window close
This commit is contained in:
Huakun Shen 2024-11-12 14:40:27 -05:00
parent fb0e5761c9
commit 7b9be980b9
No known key found for this signature in database
GPG Key ID: 967DBC3ECBD63A70
8 changed files with 34 additions and 27 deletions

View File

@ -63,7 +63,6 @@ export async function onCustomUiCmdSelect(
const newUrl = `http://${addr}` const newUrl = `http://${addr}`
url2 = `/extension/ui-iframe?url=${encodeURIComponent(newUrl)}&extPath=${encodeURIComponent(ext.extPath)}` url2 = `/extension/ui-iframe?url=${encodeURIComponent(newUrl)}&extPath=${encodeURIComponent(ext.extPath)}`
} }
console.log("URL 2", url2)
const window = launchNewExtWindow(winLabel, url2, cmd.window) const window = launchNewExtWindow(winLabel, url2, cmd.window)
window.onCloseRequested(async (event) => { window.onCloseRequested(async (event) => {
await winExtMap.unregisterExtensionFromWindow(winLabel) await winExtMap.unregisterExtensionFromWindow(winLabel)
@ -81,7 +80,6 @@ export async function onCustomUiCmdSelect(
const newUrl = `http://${addr}` const newUrl = `http://${addr}`
url2 = `/extension/ui-iframe?url=${encodeURIComponent(newUrl)}&extPath=${encodeURIComponent(ext.extPath)}` url2 = `/extension/ui-iframe?url=${encodeURIComponent(newUrl)}&extPath=${encodeURIComponent(ext.extPath)}`
} }
console.log("URL 2", url2)
goto(url2) goto(url2)
} }
appState.clearSearchTerm() appState.clearSearchTerm()

View File

@ -30,6 +30,7 @@ type API = {
dist?: string dist?: string
}) => Promise<string> }) => Promise<string>
unregisterExtensionFromWindow: (windowLabel: string) => Promise<void> unregisterExtensionFromWindow: (windowLabel: string) => Promise<void>
cleanupProcessesFromWindow: (windowLabel: string) => Promise<void>
registerProcess: (windowLabel: string, pid: number) => Promise<void> registerProcess: (windowLabel: string, pid: number) => Promise<void>
unregisterProcess: (pid: number) => Promise<void> unregisterProcess: (pid: number) => Promise<void>
} }
@ -84,11 +85,10 @@ function createWinExtMapStore(): Writable<WinExtMap> & API {
if (winExtMap[windowLabel]) { if (winExtMap[windowLabel]) {
// clean up processes spawned by extension but not killed by itself // clean up processes spawned by extension but not killed by itself
const extLabelMap = await getExtLabelMap() // realtime data from core process const extLabelMap = await getExtLabelMap() // realtime data from core process
Object.entries(extLabelMap).forEach(([label, ext]) => { if (extLabelMap[windowLabel]) {
if (label === windowLabel) { console.log("kill processes", extLabelMap[windowLabel].processes)
killProcesses(ext.processes) killProcesses(extLabelMap[windowLabel].processes)
} }
})
await unregisterExtensionWindow(windowLabel) await unregisterExtensionWindow(windowLabel)
delete winExtMap[windowLabel] delete winExtMap[windowLabel]
store.set(winExtMap) store.set(winExtMap)
@ -96,9 +96,15 @@ function createWinExtMapStore(): Writable<WinExtMap> & API {
warn(`Window ${windowLabel} does not have an extension registered`) warn(`Window ${windowLabel} does not have an extension registered`)
} }
}, },
cleanupProcessesFromWindow: async (windowLabel: string) => {
const winExtMap = get(store)
if (winExtMap[windowLabel]) {
await killProcesses(winExtMap[windowLabel].pids)
}
},
registerProcess: async (windowLabel: string, pid: number) => { registerProcess: async (windowLabel: string, pid: number) => {
const winExtMap = get(store) const winExtMap = get(store)
registerExtensionSpawnedProcess(windowLabel, pid) await registerExtensionSpawnedProcess(windowLabel, pid)
if (!winExtMap[windowLabel]) { if (!winExtMap[windowLabel]) {
throw new Error(`Window ${windowLabel} does not have an extension registered`) throw new Error(`Window ${windowLabel} does not have an extension registered`)
} }

View File

@ -29,7 +29,6 @@ export function positionToTailwindClasses(position: Position) {
if (parseOutput.output.left) { if (parseOutput.output.left) {
className += ` left-[${parseOutput.output.left / 4}rem]` className += ` left-[${parseOutput.output.left / 4}rem]`
} }
console.log(position, className)
return className return className
} }
} }

View File

@ -1,12 +1,13 @@
<script lang="ts"> <script lang="ts">
import AppContext from "@/components/context/AppContext.svelte" import AppContext from "@/components/context/AppContext.svelte"
import "../app.css" import "../app.css"
import { appConfig, appState, extensions, quickLinks } from "@/stores" import { appConfig, appState, extensions, quickLinks, winExtMap } from "@/stores"
import { initDeeplink } from "@/utils/deeplink" import { initDeeplink } from "@/utils/deeplink"
import { updateAppHotkey } from "@/utils/hotkey" import { updateAppHotkey } from "@/utils/hotkey"
import { globalKeyDownHandler, goBackOrCloseOnEscape } from "@/utils/key" import { globalKeyDownHandler, goBackOrCloseOnEscape } from "@/utils/key"
import { listenToWindowBlur } from "@/utils/tauri-events" import { listenToWindowBlur } from "@/utils/tauri-events"
import { isInMainWindow } from "@/utils/window" import { isInMainWindow } from "@/utils/window"
import { listenToKillProcessEvent, listenToRecordExtensionProcessEvent } from "@kksh/api/events"
import { import {
ModeWatcher, ModeWatcher,
themeConfigStore, themeConfigStore,
@ -78,6 +79,18 @@
}) })
) )
extensions.init() extensions.init()
unlisteners.push(
await listenToRecordExtensionProcessEvent(async (event) => {
console.log("record extension process event", event)
winExtMap.registerProcess(event.payload.windowLabel, event.payload.pid)
})
)
unlisteners.push(
await listenToKillProcessEvent((event) => {
console.log("kill process event", event)
winExtMap.unregisterProcess(event.payload.pid)
})
)
} else { } else {
} }
getCurrentWebviewWindow().show() getCurrentWebviewWindow().show()

View File

@ -5,13 +5,10 @@
import { systemCommands } from "@/cmds/system" import { systemCommands } from "@/cmds/system"
import { appConfig, appState, devStoreExts, installedStoreExts, quickLinks } from "@/stores" import { appConfig, appState, devStoreExts, installedStoreExts, quickLinks } from "@/stores"
import { cmdQueries } from "@/stores/cmdQuery" import { cmdQueries } from "@/stores/cmdQuery"
import { getActiveElementNodeName, isKeyboardEventFromInputElement } from "@/utils/dom" import { isKeyboardEventFromInputElement } from "@/utils/dom"
import Icon from "@iconify/svelte" import Icon from "@iconify/svelte"
import { openDevTools, toggleDevTools } from "@kksh/api/commands" import { toggleDevTools } from "@kksh/api/commands"
import type { ExtPackageJsonExtra } from "@kksh/api/models"
import { isExtPathInDev } from "@kksh/extension/utils"
import { Button, Command, DropdownMenu } from "@kksh/svelte5" import { Button, Command, DropdownMenu } from "@kksh/svelte5"
import type { AppConfig, AppState } from "@kksh/types"
import { import {
BuiltinCmds, BuiltinCmds,
CustomCommandInput, CustomCommandInput,
@ -20,12 +17,11 @@
QuickLinks, QuickLinks,
SystemCmds SystemCmds
} from "@kksh/ui/main" } from "@kksh/ui/main"
import type { BuiltinCmd, CmdValue, CommandLaunchers } from "@kksh/ui/types" import type { CmdValue } from "@kksh/ui/types"
import { cn, commandScore } from "@kksh/ui/utils" import { cn, commandScore } from "@kksh/ui/utils"
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow" import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
import { exit } from "@tauri-apps/plugin-process" import { exit } from "@tauri-apps/plugin-process"
import { ArrowBigUpIcon, CircleXIcon, EllipsisVerticalIcon, RefreshCcwIcon } from "lucide-svelte" import { ArrowBigUpIcon, CircleXIcon, EllipsisVerticalIcon, RefreshCcwIcon } from "lucide-svelte"
import { onMount } from "svelte"
let inputEle: HTMLInputElement | null = null let inputEle: HTMLInputElement | null = null
function onKeyDown(event: KeyboardEvent) { function onKeyDown(event: KeyboardEvent) {
@ -136,7 +132,7 @@
> >
<Icon <Icon
icon={$appConfig.hmr ? "fontisto:toggle-on" : "fontisto:toggle-off"} icon={$appConfig.hmr ? "fontisto:toggle-on" : "fontisto:toggle-off"}
class={cn("mr-1 h-5 w-5", $appConfig.hmr ? "text-green-500" : "")} class={cn("mr-1 h-5 w-5", { "text-green-500": $appConfig.hmr })}
/> />
Toggle Dev Extension HMR Toggle Dev Extension HMR
</DropdownMenu.Item> </DropdownMenu.Item>

View File

@ -112,7 +112,6 @@
} satisfies IApp } satisfies IApp
function onBackBtnClicked() { function onBackBtnClicked() {
console.log("onBackBtnClicked")
if (isInMainWindow()) { if (isInMainWindow()) {
goHome() goHome()
} else { } else {

View File

@ -56,14 +56,9 @@
let loaded = $state(false) let loaded = $state(false)
async function goBack() { async function goBack() {
console.log("goBack")
if (isInMainWindow()) { if (isInMainWindow()) {
console.log("goBack in main window")
// if in main window, then winExtMap store must contain this
// winExtMap.unregisterExtensionFromWindow(appWin.label)
goto("/") goto("/")
} else { } else {
console.log("goBack in webview window")
appWin.close() appWin.close()
} }
} }
@ -235,6 +230,7 @@
onDestroy(() => { onDestroy(() => {
unlistenRefreshWorkerExt?.() unlistenRefreshWorkerExt?.()
winExtMap.unregisterExtensionFromWindow(appWin.label)
extensionLoadingBar = false extensionLoadingBar = false
appState.setActionPanel(undefined) appState.setActionPanel(undefined)
}) })

View File

@ -38,12 +38,12 @@
<IconMultiplexer icon={cmd.icon ?? ext.kunkun.icon} class="!h-5 !w-5 shrink-0" /> <IconMultiplexer icon={cmd.icon ?? ext.kunkun.icon} class="!h-5 !w-5 shrink-0" />
<span>{cmd.name}</span> <span>{cmd.name}</span>
</span> </span>
<span class="flex gap-2"> <span class="flex gap-1">
{#if isDev} {#if isDev}
<Badge class="rounded-sm bg-green-500 px-1">Dev</Badge> <Badge class="scale-75 rounded-sm bg-green-500 px-1">Dev</Badge>
{/if} {/if}
{#if hmr} {#if hmr}
<Badge class="rounded-sm px-1">HMR</Badge> <Badge class="scale-75 rounded-sm px-1">HMR</Badge>
{/if} {/if}
</span> </span>
</Command.Item> </Command.Item>