support reset application

This commit is contained in:
pompurin404 2024-11-22 17:44:51 +08:00
parent ec3efe89c7
commit 22c35b606e
No known key found for this signature in database
10 changed files with 218 additions and 115 deletions

View File

@ -222,7 +222,7 @@ function parseSubinfo(str: string): ISubscriptionUserInfo {
} }
function isAbsolutePath(path: string): boolean { function isAbsolutePath(path: string): boolean {
return path.startsWith('/') || /^[a-zA-Z]:\\/.test(path); return path.startsWith('/') || /^[a-zA-Z]:\\/.test(path)
} }
export async function getFileStr(path: string): Promise<string> { export async function getFileStr(path: string): Promise<string> {

View File

@ -1,9 +1,10 @@
import { exec, execFile, execSync } from 'child_process' import { exec, execFile, execSync, spawn } from 'child_process'
import { dialog, nativeTheme, shell } from 'electron' import { app, dialog, nativeTheme, shell } from 'electron'
import { readFile } from 'fs/promises' import { readFile } from 'fs/promises'
import path from 'path' import path from 'path'
import { promisify } from 'util' import { promisify } from 'util'
import { import {
dataDir,
exePath, exePath,
mihomoCorePath, mihomoCorePath,
overridePath, overridePath,
@ -112,3 +113,33 @@ export function createElevateTask(): void {
`%SystemRoot%\\System32\\schtasks.exe /create /tn "mihomo-party-run" /xml "${taskFilePath}" /f` `%SystemRoot%\\System32\\schtasks.exe /create /tn "mihomo-party-run" /xml "${taskFilePath}" /f`
) )
} }
export function resetAppConfig(): void {
if (process.platform === 'win32') {
spawn(
'cmd',
[
'/C',
`"timeout /t 2 /nobreak >nul && rmdir /s /q "${dataDir()}" && start "" "${exePath()}""`
],
{
shell: true,
detached: true
}
).unref()
} else {
const script = `while kill -0 ${process.pid} 2>/dev/null; do
sleep 0.1
done
rm -rf '${dataDir()}'
${process.argv.join(' ')} & disown
exit
`
spawn('sh', ['-c', `"${script}"`], {
shell: true,
detached: true,
stdio: 'ignore'
})
}
app.quit()
}

View File

@ -61,6 +61,7 @@ import {
openFile, openFile,
openUWPTool, openUWPTool,
readTextFile, readTextFile,
resetAppConfig,
setNativeTheme, setNativeTheme,
setupFirewall setupFirewall
} from '../sys/misc' } from '../sys/misc'
@ -246,6 +247,7 @@ export function registerIpcMainHandlers(): void {
ipcMain.handle('alert', (_e, msg) => { ipcMain.handle('alert', (_e, msg) => {
dialog.showErrorBox('Mihomo Party', msg) dialog.showErrorBox('Mihomo Party', msg)
}) })
ipcMain.handle('resetAppConfig', resetAppConfig)
ipcMain.handle('relaunchApp', () => { ipcMain.handle('relaunchApp', () => {
app.relaunch() app.relaunch()
app.quit() app.quit()

View File

@ -1,4 +1,15 @@
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button, Dropdown, DropdownTrigger, DropdownMenu, DropdownItem } from '@nextui-org/react' import {
Modal,
ModalContent,
ModalHeader,
ModalBody,
ModalFooter,
Button,
Dropdown,
DropdownTrigger,
DropdownMenu,
DropdownItem
} from '@nextui-org/react'
import React from 'react' import React from 'react'
import SettingItem from '../base/base-setting-item' import SettingItem from '../base/base-setting-item'
import { calcTraffic } from '@renderer/utils/calc' import { calcTraffic } from '@renderer/utils/calc'
@ -17,50 +28,70 @@ const CopyableSettingItem: React.FC<{
prefix?: string[] prefix?: string[]
suffix?: string suffix?: string
}> = ({ title, value, displayName, prefix = [], suffix = '' }) => { }> = ({ title, value, displayName, prefix = [], suffix = '' }) => {
const getSubDomains = (domain: string) => const getSubDomains = (domain: string): string[] =>
domain.split('.').length <= 2 domain.split('.').length <= 2
? [domain] ? [domain]
: domain.split('.').map((_, i, parts) => parts.slice(i).join('.')).slice(0, -1) : domain
.split('.')
.map((_, i, parts) => parts.slice(i).join('.'))
.slice(0, -1)
const menuItems = [ const menuItems = [
{ key: 'raw', text: displayName || (Array.isArray(value) ? value.join(', ') : value) }, { key: 'raw', text: displayName || (Array.isArray(value) ? value.join(', ') : value) },
...(Array.isArray(value) && value.length === prefix.length ...(Array.isArray(value) && value.length === prefix.length
? prefix.map((p, i) => value[i] ? ({ ? prefix
.map((p, i) =>
value[i]
? {
key: `${p},${p === 'IP-ASN' ? value[i].split(' ')[0] : value[i]}${suffix}`, key: `${p},${p === 'IP-ASN' ? value[i].split(' ')[0] : value[i]}${suffix}`,
text: `${p},${p === 'IP-ASN' ? value[i].split(' ')[0] : value[i]}${suffix}` text: `${p},${p === 'IP-ASN' ? value[i].split(' ')[0] : value[i]}${suffix}`
}) : null).filter(Boolean) }
: prefix.flatMap(p => : null
(Array.isArray(value) )
? value.map(v => p === 'DOMAIN-SUFFIX' .filter(Boolean)
? getSubDomains(v).map(subV => ({ : prefix.flatMap((p) =>
Array.isArray(value)
? value
.map((v) =>
p === 'DOMAIN-SUFFIX'
? getSubDomains(v).map((subV) => ({
key: `${p},${subV}${suffix}`, key: `${p},${subV}${suffix}`,
text: `${p},${subV}${suffix}` text: `${p},${subV}${suffix}`
})) }))
: p === 'IP-ASN' || p === 'SRC-IP-ASN' : p === 'IP-ASN' || p === 'SRC-IP-ASN'
? [{ ? [
{
key: `${p},${v.split(' ')[0]}${suffix}`, key: `${p},${v.split(' ')[0]}${suffix}`,
text: `${p},${v.split(' ')[0]}${suffix}` text: `${p},${v.split(' ')[0]}${suffix}`
}] }
: [{ ]
: [
{
key: `${p},${v}${suffix}`, key: `${p},${v}${suffix}`,
text: `${p},${v}${suffix}` text: `${p},${v}${suffix}`
}] }
).flat() ]
)
.flat()
: p === 'DOMAIN-SUFFIX' : p === 'DOMAIN-SUFFIX'
? getSubDomains(value).map(v => ({ ? getSubDomains(value).map((v) => ({
key: `${p},${v}${suffix}`, key: `${p},${v}${suffix}`,
text: `${p},${v}${suffix}` text: `${p},${v}${suffix}`
})) }))
: p === 'IP-ASN' || p === 'SRC-IP-ASN' : p === 'IP-ASN' || p === 'SRC-IP-ASN'
? [{ ? [
{
key: `${p},${value.split(' ')[0]}${suffix}`, key: `${p},${value.split(' ')[0]}${suffix}`,
text: `${p},${value.split(' ')[0]}${suffix}` text: `${p},${value.split(' ')[0]}${suffix}`
}] }
: [{ ]
: [
{
key: `${p},${value}${suffix}`, key: `${p},${value}${suffix}`,
text: `${p},${value}${suffix}` text: `${p},${value}${suffix}`
}] }
))) ]
))
] ]
return ( return (
@ -69,16 +100,20 @@ const CopyableSettingItem: React.FC<{
actions={ actions={
<Dropdown> <Dropdown>
<DropdownTrigger> <DropdownTrigger>
<Button title='复制规则' isIconOnly size='sm' variant='light'> <Button title="复制规则" isIconOnly size="sm" variant="light">
<BiCopy className='text-lg' /> <BiCopy className="text-lg" />
</Button> </Button>
</DropdownTrigger> </DropdownTrigger>
<DropdownMenu <DropdownMenu
onAction={key => onAction={(key) =>
navigator.clipboard.writeText(key === 'raw' ? (Array.isArray(value) ? value.join(', ') : value) : key as string) navigator.clipboard.writeText(
key === 'raw' ? (Array.isArray(value) ? value.join(', ') : value) : (key as string)
)
} }
> >
{menuItems.filter(item => item !== null).map(({ key, text }) => ( {menuItems
.filter((item) => item !== null)
.map(({ key, text }) => (
<DropdownItem key={key}>{text}</DropdownItem> <DropdownItem key={key}>{text}</DropdownItem>
))} ))}
</DropdownMenu> </DropdownMenu>
@ -94,170 +129,174 @@ const ConnectionDetailModal: React.FC<Props> = (props) => {
const { connection, onClose } = props const { connection, onClose } = props
return ( return (
<Modal <Modal
backdrop='blur' backdrop="blur"
classNames={{ backdrop: 'top-[48px]' }} classNames={{ backdrop: 'top-[48px]' }}
size='xl' size="xl"
hideCloseButton hideCloseButton
isOpen={true} isOpen={true}
onOpenChange={onClose} onOpenChange={onClose}
scrollBehavior='inside' scrollBehavior="inside"
> >
<ModalContent className='flag-emoji break-all'> <ModalContent className="flag-emoji break-all">
<ModalHeader className='flex app-drag'></ModalHeader> <ModalHeader className="flex app-drag"></ModalHeader>
<ModalBody> <ModalBody>
<SettingItem title='连接建立时间'>{dayjs(connection.start).fromNow()}</SettingItem> <SettingItem title="连接建立时间">{dayjs(connection.start).fromNow()}</SettingItem>
<SettingItem title='规则'> <SettingItem title="规则">
{connection.rule} {connection.rule}
{connection.rulePayload ? `(${connection.rulePayload})` : ''} {connection.rulePayload ? `(${connection.rulePayload})` : ''}
</SettingItem> </SettingItem>
<SettingItem title='代理链'>{[...connection.chains].reverse().join('>>')}</SettingItem> <SettingItem title="代理链">{[...connection.chains].reverse().join('>>')}</SettingItem>
<SettingItem title='上传速度'>{calcTraffic(connection.uploadSpeed || 0)}/s</SettingItem> <SettingItem title="上传速度">{calcTraffic(connection.uploadSpeed || 0)}/s</SettingItem>
<SettingItem title='下载速度'>{calcTraffic(connection.downloadSpeed || 0)}/s</SettingItem> <SettingItem title="下载速度">{calcTraffic(connection.downloadSpeed || 0)}/s</SettingItem>
<SettingItem title='上传量'>{calcTraffic(connection.upload)}</SettingItem> <SettingItem title="上传量">{calcTraffic(connection.upload)}</SettingItem>
<SettingItem title='下载量'>{calcTraffic(connection.download)}</SettingItem> <SettingItem title="下载量">{calcTraffic(connection.download)}</SettingItem>
<CopyableSettingItem <CopyableSettingItem
title='连接类型' title="连接类型"
value={[connection.metadata.type, connection.metadata.network]} value={[connection.metadata.type, connection.metadata.network]}
displayName={`${connection.metadata.type}(${connection.metadata.network})`} displayName={`${connection.metadata.type}(${connection.metadata.network})`}
prefix={['IN-TYPE', 'NETWORK']} prefix={['IN-TYPE', 'NETWORK']}
/> />
{connection.metadata.host && ( {connection.metadata.host && (
<CopyableSettingItem <CopyableSettingItem
title='主机' title="主机"
value={connection.metadata.host} value={connection.metadata.host}
prefix={['DOMAIN', 'DOMAIN-SUFFIX']} prefix={['DOMAIN', 'DOMAIN-SUFFIX']}
/> />
)} )}
{connection.metadata.sniffHost && ( {connection.metadata.sniffHost && (
<CopyableSettingItem <CopyableSettingItem
title='嗅探主机' title="嗅探主机"
value={connection.metadata.sniffHost} value={connection.metadata.sniffHost}
prefix={['DOMAIN', 'DOMAIN-SUFFIX']} prefix={['DOMAIN', 'DOMAIN-SUFFIX']}
/> />
)} )}
{connection.metadata.process && ( {connection.metadata.process && (
<CopyableSettingItem <CopyableSettingItem
title='进程名' title="进程名"
value={[connection.metadata.process, connection.metadata.uid ? connection.metadata.uid.toString() : '']} value={[
connection.metadata.process,
connection.metadata.uid ? connection.metadata.uid.toString() : ''
]}
displayName={`${connection.metadata.process}${connection.metadata.uid ? `(${connection.metadata.uid})` : ''}`} displayName={`${connection.metadata.process}${connection.metadata.uid ? `(${connection.metadata.uid})` : ''}`}
prefix={['PROCESS-NAME', 'UID']} prefix={['PROCESS-NAME', 'UID']}
/> />
)} )}
{connection.metadata.processPath && ( {connection.metadata.processPath && (
<CopyableSettingItem <CopyableSettingItem
title='进程路径' title="进程路径"
value={connection.metadata.processPath} value={connection.metadata.processPath}
prefix={['PROCESS-PATH']} prefix={['PROCESS-PATH']}
/> />
)} )}
{connection.metadata.sourceIP && ( {connection.metadata.sourceIP && (
<CopyableSettingItem <CopyableSettingItem
title='来源IP' title="来源IP"
value={connection.metadata.sourceIP} value={connection.metadata.sourceIP}
prefix={['SRC-IP-CIDR']} prefix={['SRC-IP-CIDR']}
suffix='/32' suffix="/32"
/> />
)} )}
{connection.metadata.sourceGeoIP && connection.metadata.sourceGeoIP.length > 0 && ( {connection.metadata.sourceGeoIP && connection.metadata.sourceGeoIP.length > 0 && (
<CopyableSettingItem <CopyableSettingItem
title='来源GeoIP' title="来源GeoIP"
value={connection.metadata.sourceGeoIP} value={connection.metadata.sourceGeoIP}
prefix={['SRC-GEOIP']} prefix={['SRC-GEOIP']}
/> />
)} )}
{connection.metadata.sourceIPASN && ( {connection.metadata.sourceIPASN && (
<CopyableSettingItem <CopyableSettingItem
title='来源ASN' title="来源ASN"
value={connection.metadata.sourceIPASN} value={connection.metadata.sourceIPASN}
prefix={['SRC-IP-ASN']} prefix={['SRC-IP-ASN']}
/> />
)} )}
{connection.metadata.destinationIP && ( {connection.metadata.destinationIP && (
<CopyableSettingItem <CopyableSettingItem
title='目标IP' title="目标IP"
value={connection.metadata.destinationIP} value={connection.metadata.destinationIP}
prefix={['IP-CIDR']} prefix={['IP-CIDR']}
suffix='/32' suffix="/32"
/> />
)} )}
{connection.metadata.destinationGeoIP && connection.metadata.destinationGeoIP.length > 0 && ( {connection.metadata.destinationGeoIP &&
connection.metadata.destinationGeoIP.length > 0 && (
<CopyableSettingItem <CopyableSettingItem
title='目标GeoIP' title="目标GeoIP"
value={connection.metadata.destinationGeoIP} value={connection.metadata.destinationGeoIP}
prefix={['GEOIP']} prefix={['GEOIP']}
/> />
)} )}
{connection.metadata.destinationIPASN && ( {connection.metadata.destinationIPASN && (
<CopyableSettingItem <CopyableSettingItem
title='目标ASN' title="目标ASN"
value={connection.metadata.destinationIPASN} value={connection.metadata.destinationIPASN}
prefix={['IP-ASN']} prefix={['IP-ASN']}
/> />
)} )}
{connection.metadata.sourcePort && ( {connection.metadata.sourcePort && (
<CopyableSettingItem <CopyableSettingItem
title='来源端口' title="来源端口"
value={connection.metadata.sourcePort} value={connection.metadata.sourcePort}
prefix={['SRC-PORT']} prefix={['SRC-PORT']}
/> />
)} )}
{connection.metadata.destinationPort && ( {connection.metadata.destinationPort && (
<CopyableSettingItem <CopyableSettingItem
title='目标端口' title="目标端口"
value={connection.metadata.destinationPort} value={connection.metadata.destinationPort}
prefix={['DST-PORT']} prefix={['DST-PORT']}
/> />
)} )}
{connection.metadata.inboundIP && ( {connection.metadata.inboundIP && (
<CopyableSettingItem <CopyableSettingItem
title='入站IP' title="入站IP"
value={connection.metadata.inboundIP} value={connection.metadata.inboundIP}
prefix={['SRC-IP-CIDR']} prefix={['SRC-IP-CIDR']}
/> />
)} )}
{connection.metadata.inboundPort && ( {connection.metadata.inboundPort && (
<CopyableSettingItem <CopyableSettingItem
title='入站端口' title="入站端口"
value={connection.metadata.inboundPort} value={connection.metadata.inboundPort}
prefix={['SRC-PORT']} prefix={['SRC-PORT']}
/> />
)} )}
{connection.metadata.inboundName && ( {connection.metadata.inboundName && (
<CopyableSettingItem <CopyableSettingItem
title='入站名称' title="入站名称"
value={connection.metadata.inboundName} value={connection.metadata.inboundName}
prefix={['IN-NAME']} prefix={['IN-NAME']}
/> />
)} )}
{connection.metadata.inboundUser && ( {connection.metadata.inboundUser && (
<CopyableSettingItem <CopyableSettingItem
title='入站用户' title="入站用户"
value={connection.metadata.inboundUser} value={connection.metadata.inboundUser}
prefix={['IN-USER']} prefix={['IN-USER']}
/> />
)} )}
<CopyableSettingItem <CopyableSettingItem
title='DSCP' title="DSCP"
value={connection.metadata.dscp.toString()} value={connection.metadata.dscp.toString()}
prefix={['DSCP']} prefix={['DSCP']}
/> />
{connection.metadata.remoteDestination && ( {connection.metadata.remoteDestination && (
<SettingItem title='远程目标'>{connection.metadata.remoteDestination}</SettingItem> <SettingItem title="远程目标">{connection.metadata.remoteDestination}</SettingItem>
)} )}
{connection.metadata.dnsMode && ( {connection.metadata.dnsMode && (
<SettingItem title='DNS模式'>{connection.metadata.dnsMode}</SettingItem> <SettingItem title="DNS模式">{connection.metadata.dnsMode}</SettingItem>
)} )}
{connection.metadata.specialProxy && ( {connection.metadata.specialProxy && (
<SettingItem title='特殊代理'>{connection.metadata.specialProxy}</SettingItem> <SettingItem title="特殊代理">{connection.metadata.specialProxy}</SettingItem>
)} )}
{connection.metadata.specialRules && ( {connection.metadata.specialRules && (
<SettingItem title='特殊规则'>{connection.metadata.specialRules}</SettingItem> <SettingItem title="特殊规则">{connection.metadata.specialRules}</SettingItem>
)} )}
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button size='sm' variant='light' onPress={onClose}> <Button size="sm" variant="light" onPress={onClose}>
</Button> </Button>
</ModalFooter> </ModalFooter>

View File

@ -22,7 +22,7 @@ const ProxyProvider: React.FC = () => {
const [ShowType, setShowType] = useState('') const [ShowType, setShowType] = useState('')
useEffect(() => { useEffect(() => {
const fetchProviderPath = async (name: string) => { const fetchProviderPath = async (name: string): Promise<void> => {
try { try {
const providers = await getRuntimeConfig() const providers = await getRuntimeConfig()
const provider = providers['proxy-providers'][name] const provider = providers['proxy-providers'][name]

View File

@ -30,7 +30,7 @@ const RuleProvider: React.FC = () => {
const [updating, setUpdating] = useState(Array(providers.length).fill(false)) const [updating, setUpdating] = useState(Array(providers.length).fill(false))
useEffect(() => { useEffect(() => {
const fetchProviderPath = async (name: string) => { const fetchProviderPath = async (name: string): Promise<void> => {
try { try {
const providers = await getRuntimeConfig() const providers = await getRuntimeConfig()
const provider = providers['rule-providers'][name] const provider = providers['rule-providers'][name]

View File

@ -13,7 +13,7 @@ interface Props {
const Viewer: React.FC<Props> = (props) => { const Viewer: React.FC<Props> = (props) => {
const { type, path, format, onClose } = props const { type, path, format, onClose } = props
const [currData, setCurrData] = useState('') const [currData, setCurrData] = useState('')
const language: Language = (!format || format === 'YamlRule') ? 'yaml' : 'text' const language: Language = !format || format === 'YamlRule' ? 'yaml' : 'text'
const getContent = async (): Promise<void> => { const getContent = async (): Promise<void> => {
setCurrData(await getFileStr(path)) setCurrData(await getFileStr(path))
@ -36,13 +36,18 @@ const Viewer: React.FC<Props> = (props) => {
<ModalContent className="h-full w-[calc(100%-100px)]"> <ModalContent className="h-full w-[calc(100%-100px)]">
<ModalHeader className="flex pb-0 app-drag">Provider </ModalHeader> <ModalHeader className="flex pb-0 app-drag">Provider </ModalHeader>
<ModalBody className="h-full"> <ModalBody className="h-full">
<BaseEditor language={language} value={currData} readOnly={type != 'File'} onChange={(value) => setCurrData(value)}/> <BaseEditor
language={language}
value={currData}
readOnly={type != 'File'}
onChange={(value) => setCurrData(value)}
/>
</ModalBody> </ModalBody>
<ModalFooter className="pt-0"> <ModalFooter className="pt-0">
<Button size="sm" variant="light" onPress={onClose}> <Button size="sm" variant="light" onPress={onClose}>
</Button> </Button>
{type == 'File' && {type == 'File' && (
<Button <Button
size="sm" size="sm"
color="primary" color="primary"
@ -52,7 +57,8 @@ const Viewer: React.FC<Props> = (props) => {
}} }}
> >
</Button>} </Button>
)}
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
</Modal> </Modal>

View File

@ -1,7 +1,13 @@
import { Button, Tooltip } from '@nextui-org/react' import { Button, Tooltip } from '@nextui-org/react'
import SettingCard from '../base/base-setting-card' import SettingCard from '../base/base-setting-card'
import SettingItem from '../base/base-setting-item' import SettingItem from '../base/base-setting-item'
import { checkUpdate, createHeapSnapshot, quitApp, quitWithoutCore } from '@renderer/utils/ipc' import {
checkUpdate,
createHeapSnapshot,
quitApp,
quitWithoutCore,
resetAppConfig
} from '@renderer/utils/ipc'
import { useState } from 'react' import { useState } from 'react'
import UpdaterModal from '../updater/updater-modal' import UpdaterModal from '../updater/updater-modal'
import { version } from '@renderer/utils/init' import { version } from '@renderer/utils/init'
@ -54,6 +60,21 @@ const Actions: React.FC = () => {
</Button> </Button>
</SettingItem> </SettingItem>
<SettingItem
title="重置软件"
actions={
<Tooltip content="删除所有配置,将软件恢复初始状态">
<Button isIconOnly size="sm" variant="light">
<IoIosHelpCircle className="text-lg" />
</Button>
</Tooltip>
}
divider
>
<Button size="sm" onPress={resetAppConfig}>
</Button>
</SettingItem>
<SettingItem <SettingItem
title="创建堆快照" title="创建堆快照"
actions={ actions={

View File

@ -1,31 +1,31 @@
import { MD5 } from 'crypto-js'; import { MD5 } from 'crypto-js'
export class HashType { export class HashType {
private hashValue: string; private hashValue: string
constructor(hash: string) { constructor(hash: string) {
this.hashValue = hash; this.hashValue = hash
} }
static makeHash(data: string): HashType { static makeHash(data: string): HashType {
const hash = MD5(data).toString(); const hash = MD5(data).toString()
return new HashType(hash); return new HashType(hash)
} }
equal(hash: HashType): boolean { equal(hash: HashType): boolean {
return this.hashValue === hash.hashValue; return this.hashValue === hash.hashValue
} }
toString(): string { toString(): string {
return this.hashValue; return this.hashValue
} }
isValid(): boolean { isValid(): boolean {
return this.hashValue.length === 32; return this.hashValue.length === 32
} }
} }
export function getHash(name: string): string { export function getHash(name: string): string {
const hash = HashType.makeHash(name); const hash = HashType.makeHash(name)
return hash.toString(); return hash.toString()
} }

View File

@ -383,6 +383,10 @@ export async function openDevTools(): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('openDevTools')) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('openDevTools'))
} }
export async function resetAppConfig(): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('resetAppConfig'))
}
export async function createHeapSnapshot(): Promise<void> { export async function createHeapSnapshot(): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('createHeapSnapshot')) return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('createHeapSnapshot'))
} }