From b7729c0d3b18e65df5b8bb64a48c5ef9bf949e85 Mon Sep 17 00:00:00 2001 From: PalmDevs Date: Wed, 1 Jan 2025 04:14:02 +0700 Subject: [PATCH] feat(debugger): merge with `developer-settings` plugin, improve server logging --- bun.lockb | Bin 325292 -> 325580 bytes scripts/debugger.mjs | 49 ++-- src/plugins/debugger/index.tsx | 115 ---------- src/plugins/debugger/pages/Debugger.tsx | 95 -------- src/plugins/developer-settings/debugger.d.ts | 14 ++ .../debugger.ts | 212 ++++++++---------- src/plugins/developer-settings/index.tsx | 63 ++++++ .../developer-settings/pages/Developer.tsx | 167 +++++++++----- src/plugins/index.ts | 1 - types.d.ts | 2 + 10 files changed, 320 insertions(+), 398 deletions(-) delete mode 100644 src/plugins/debugger/index.tsx delete mode 100644 src/plugins/debugger/pages/Debugger.tsx create mode 100644 src/plugins/developer-settings/debugger.d.ts rename src/plugins/{debugger => developer-settings}/debugger.ts (60%) diff --git a/bun.lockb b/bun.lockb index 3c12cc6361e6f0130d5ada2c798a628322714d57..17cabe7ce8c8a363135bd0fca89348a7342af0d7 100644 GIT binary patch delta 50832 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 Wc5(CU6(Do3k?LKP$rN console.info(clientColorify(null, message)) const logAsClientWarn = message => console.warn(clientColorify('warn', message)) const logAsClientError = message => console.error(clientColorify('error', message)) -const copyPrompt = ' --copy' -const clearHistoryPrompt = '--ch' +const copyPrompt = '--copy' +const clearHistoryPrompt = '--clear' export function serve() { let websocketOpen = false - let awaitingReply + let nextReply const wss = new WebSocketServer({ port: 9090, @@ -46,25 +47,25 @@ export function serve() { if (websocketOpen) return websocketOpen = true - logAsDebugger('Starting debugger session') + logAsDebugger('Client connected') ws.on('message', data => { try { /** @type {{ level: "info" | "warn" | "error", message: string, nonce?: string }} */ const json = JSON.parse(data.toString()) - if (awaitingReply?.cb && awaitingReply?.nonce && awaitingReply.nonce === json.nonce) { - if (json.level === 'info' && awaitingReply.toCopy) { + if (nextReply?.cb && nextReply?.nonce && nextReply.nonce === json.nonce) { + if (json.level === 'info' && nextReply.toCopy) { clipboardy.write(json.message) - awaitingReply.cb(null, debuggerColorify('Copied result to clipboard')) + nextReply.cb(null, debuggerColorify('Copied result to clipboard')) } else - awaitingReply.cb( + nextReply.cb( null, json.level === 'error' ? clientColorify('error', json.message) : clientColorify(null, json.message), ) - awaitingReply = null + nextReply = null isPrompting = true } else { if (json.level === 'error') logAsClientError(json.message) @@ -88,11 +89,11 @@ export function serve() { const code = input.trim() if (code === clearHistoryPrompt) { writeFile(join(debuggerHistoryPath, 'history.txt'), '') - logAsDebugger('Cleared repl history') + logAsDebugger('Cleared REPL history') return cb() } - awaitingReply = { + nextReply = { nonce: crypto.randomUUID(), cb, toCopy: code.endsWith(copyPrompt), @@ -100,7 +101,7 @@ export function serve() { ws.send( JSON.stringify({ code: code.endsWith(copyPrompt) ? code.slice(0, -copyPrompt.length) : code, - nonce: awaitingReply.nonce, + nonce: nextReply.nonce, }), ) } catch (e) { @@ -116,19 +117,27 @@ export function serve() { rl.on('close', () => { isPrompting = false ws.close() - logAsDebugger('Closing debugger, press Ctrl+C to exit') }) ws.on('close', () => { - logAsDebugger('Websocket was closed') + logAsDebugger('Client disconnected') rl.close() websocketOpen = false }) }) - logAsDebugger('Debugger ready at :9090') - logAsDebugger(`Add${chalk.bold(copyPrompt)} to your prompt to copy the result to clipboard`) - logAsDebugger(`Type ${chalk.bold(clearHistoryPrompt)} to clear your repl history `) + console.info(chalk.red('\nDebugger ready, available on:\n')) + const netInterfaces = os.networkInterfaces() + for (const netinterfaces of Object.values(netInterfaces)) { + for (const details of netinterfaces || []) { + if (details.family !== 'IPv4') continue + const port = chalk.yellowBright(wss.address()?.port.toString()) + console.info(` ${chalk.gray('http://')}${details.address}:${port}`) + } + } + + console.log(chalk.gray.underline(`\nRun with ${chalk.bold.white(copyPrompt)} to your prompt to copy the result to clipboard`)) + console.log(chalk.gray.underline(`Run with ${chalk.bold.white(clearHistoryPrompt)} to clear your REPL history\n`)) return wss } diff --git a/src/plugins/debugger/index.tsx b/src/plugins/debugger/index.tsx deleted file mode 100644 index 6291bb0..0000000 --- a/src/plugins/debugger/index.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { getAssetIndexByName } from '@revenge-mod/assets' -import type { PluginContextFor } from '@revenge-mod/plugins' -import { sleep } from '@revenge-mod/utils/functions' -import { registerPlugin } from 'libraries/plugins/src/internals' -import DebuggerSettingsPage from './pages/Debugger' -import { connectToDebugger, DebuggerContext } from './debugger' -import { BundleUpdaterManager } from '@revenge-mod/modules/native' - -const plugin = registerPlugin<{ - connectOnStartup: boolean - debuggerUrl: string -}>( - { - name: 'Debugger', - author: 'Revenge', - description: 'A simple WebSocket debugger for Revenge to make development easier', - id: 'revenge.debugger', - version: '1.0.0', - icon: 'LinkIcon', - async afterAppRender(context) { - const { - revenge: { - ui: { settings: sui }, - }, - patcher, - cleanup, - storage: { connectOnStartup, debuggerUrl }, - } = context - - if (connectOnStartup) connectToDebugger(debuggerUrl, context) - - // Wait for the section to be added by the Settings plugin - await sleep(0) - - // biome-ignore lint/suspicious/noExplicitAny: globalThis can be anything - const win = globalThis as any - - const doCleanup = new Set<() => void>() - - cleanup( - sui.addRowsToSection('Revenge', { - Debugger: { - type: 'route', - label: 'Debugger', - icon: getAssetIndexByName('LinkIcon'), - component: () => ( - - - - ), - }, - }), - - (() => { - win.debgr = { - reload: () => BundleUpdaterManager.reload(), - patcher: { - // biome-ignore lint/suspicious/noExplicitAny: These arguments can be anything lol - snipe: (object: any, key: any, callback?: (args: unknown) => void) => { - doCleanup.add( - patcher.after( - object, - key, - callback ?? ((args, ret) => console.log('[SNIPER]', args, ret)), - 'debgr.patcher.snipe', - ), - ) - }, - // biome-ignore lint/suspicious/noExplicitAny: These arguments can be anything lol 2 - noop: (object: any, key: any) => { - doCleanup.add(patcher.instead(object, key, () => void 0, 'debgr.patcher.noop')) - }, - wipe: () => { - for (const c of doCleanup) c() - doCleanup.clear() - }, - }, - } - - return () => (win.debgr = undefined) - })(), - - () => { - for (const c of doCleanup) c() - }, - - patcher.before( - win, - 'nativeLoggingHook', - ([message, level]) => { - if (DebuggerContext.ws?.readyState === WebSocket.OPEN) - DebuggerContext.ws.send( - JSON.stringify({ - level: level === 3 ? 'error' : level === 2 ? 'warn' : 'info', - message, - }), - ) - }, - 'loggerPatch', - ), - ) - }, - initializeStorage() { - return { - connectOnStartup: false, - debuggerUrl: 'localhost:9090', - } - }, - }, - true, - true, -) - -export type DebuggerContextType = PluginContextFor -export const PluginContext = React.createContext(null!) diff --git a/src/plugins/debugger/pages/Debugger.tsx b/src/plugins/debugger/pages/Debugger.tsx deleted file mode 100644 index 8a00a9b..0000000 --- a/src/plugins/debugger/pages/Debugger.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { useContext, useEffect, useRef, useState } from 'react' -import { PluginContext } from '..' -import { useObservable } from '@revenge-mod/storage' -import { ScrollView } from 'react-native' -import PageWrapper from 'src/plugins/settings/pages/(Wrapper)' -import { - Stack, - TableRow, - TableRowGroup, - TableRowIcon, - TableSwitchRow, - TextInput, -} from '@revenge-mod/modules/common/components' -import { toasts } from '@revenge-mod/modules/common' -import { - connectToDebugger, - DebuggerContext, - DebuggerEvents, - disconnectFromDebugger, - type DebuggerEventsListeners, -} from '../debugger' - -export default function DebuggerSettingsPage() { - const context = useContext(PluginContext) - const { - storage, - revenge: { assets }, - } = context - useObservable([storage]) - - const tempDebuggerUrl = useRef(storage.debuggerUrl || 'localhost:9090') - const [connected, setConnected] = useState(DebuggerContext.connected) - - useEffect(() => { - const listener: DebuggerEventsListeners['*'] = evt => { - if (evt === 'connect') setConnected(true) - else setConnected(false) - } - - DebuggerEvents.on('*', listener) - - return () => void DebuggerEvents.off('*', listener) - }, []) - - return ( - - - - (tempDebuggerUrl.current = text)} - onBlur={() => { - if (tempDebuggerUrl.current === storage.debuggerUrl) return - storage.debuggerUrl = tempDebuggerUrl.current - - toasts.open({ - key: 'revenge.debugger.savedurl', - content: 'Saved debugger URL!', - }) - }} - returnKeyType="done" - /> - {/* Rerender when connected changes */} - - {connected ? ( - } - onPress={() => disconnectFromDebugger()} - /> - ) : ( - } - onPress={() => connectToDebugger(storage.debuggerUrl, context)} - /> - )} - } - value={storage.connectOnStartup} - onValueChange={v => (storage.connectOnStartup = v)} - /> - - - - - ) -} diff --git a/src/plugins/developer-settings/debugger.d.ts b/src/plugins/developer-settings/debugger.d.ts new file mode 100644 index 0000000..a3ea97d --- /dev/null +++ b/src/plugins/developer-settings/debugger.d.ts @@ -0,0 +1,14 @@ +declare global { + var dbgr: { + reload(): void + patcher: { + // biome-ignore lint/suspicious/noExplicitAny: The object can be anything + snipe(object: any, key: string, callback?: (args: unknown) => void): void + // biome-ignore lint/suspicious/noExplicitAny: The object can be anything + noop(object: any, key: string): void + wipe(): void + } + } | undefined +} + +export {} \ No newline at end of file diff --git a/src/plugins/debugger/debugger.ts b/src/plugins/developer-settings/debugger.ts similarity index 60% rename from src/plugins/debugger/debugger.ts rename to src/plugins/developer-settings/debugger.ts index 802c9a7..734d117 100644 --- a/src/plugins/debugger/debugger.ts +++ b/src/plugins/developer-settings/debugger.ts @@ -1,116 +1,96 @@ -import { toasts } from '@revenge-mod/modules/common' -import { EventEmitter } from 'events' -import type { DebuggerContextType } from '.' - -export const DebuggerEvents = new EventEmitter() - -export type DebuggerEventsListeners = { - connect: () => void - disconnect: () => void - // biome-ignore lint/suspicious/noExplicitAny: Anything can be thrown - error: (err: any) => void - // biome-ignore lint/suspicious/noExplicitAny: Anything can be thrown - '*': (event: keyof DebuggerEventsListeners, err?: any) => void -} - -export const DebuggerContext = { - ws: undefined, - connected: false, -} as { - ws: WebSocket | undefined - connected: boolean -} - -export function disconnectFromDebugger() { - DebuggerContext.ws!.close() - DebuggerContext.connected = false -} - -export function connectToDebugger(addr: string, context: DebuggerContextType) { - const ws = (DebuggerContext.ws = new WebSocket(`ws://${addr}`)) - - ws.addEventListener('open', () => { - DebuggerContext.connected = true - DebuggerEvents.emit('connect') - DebuggerEvents.emit('*', 'connect') - - toasts.open({ - key: 'revenge.debugger.connected', - content: 'Connected to debugger!', - }) - }) - - ws.addEventListener('close', () => { - DebuggerContext.connected = false - DebuggerEvents.emit('disconnect') - DebuggerEvents.emit('*', 'disconnect') - - toasts.open({ - key: 'revenge.debugger.disconnected', - content: 'Disconnected from debugger!', - }) - }) - - ws.addEventListener('error', e => { - DebuggerContext.connected = false - DebuggerEvents.emit('error', e) - DebuggerEvents.emit('*', 'error', e) - - toasts.open({ - key: 'revenge.debugger.errored', - content: 'Debugger errored!', - }) - }) - - ws.addEventListener('message', e => { - try { - const json = JSON.parse(e.data) as { - code: string - nonce: string - } - - if (typeof json.code === 'string' && typeof json.nonce === 'string') { - let res: unknown - try { - // biome-ignore lint/security/noGlobalEval: This is intentional - res = globalThis.eval(json.code) - } catch (e) { - res = e - } - - const inspect = - context.revenge.modules.findProp< - (val: unknown, opts?: { depth?: number; showHidden?: boolean; color?: boolean }) => string - >('inspect')! - - try { - if (res instanceof Error) - ws.send( - JSON.stringify({ - level: 'error', - message: String(res), - nonce: json.nonce, - }), - ) - else { - ws.send( - JSON.stringify({ - level: 'info', - message: inspect(res, { showHidden: true }), - nonce: json.nonce, - }), - ) - } - } catch (e) { - ws.send( - JSON.stringify({ - level: 'error', - message: `DebuggerInternalError: ${String(e)}`, - nonce: json.nonce, - }), - ) - } - } - } catch {} - }) -} +import { EventEmitter } from 'events' +import type { RevengeLibrary } from '@revenge-mod/revenge' + +export const DebuggerEvents = new EventEmitter() + +export type DebuggerEventsListeners = { + connect: () => void + disconnect: () => void + // biome-ignore lint/suspicious/noExplicitAny: Anything can be thrown + error: (err: any) => void + // biome-ignore lint/suspicious/noExplicitAny: Anything can be thrown + '*': (event: keyof DebuggerEventsListeners, err?: any) => void +} + +export const DebuggerContext = { + ws: undefined, + connected: false, +} as { + ws: WebSocket | undefined + connected: boolean +} + +export function disconnectFromDebugger() { + DebuggerContext.ws!.close() + DebuggerContext.connected = false +} + +export function connectToDebugger(addr: string, revenge: RevengeLibrary) { + const ws = (DebuggerContext.ws = new WebSocket(`ws://${addr}`)) + + ws.addEventListener('open', () => { + DebuggerContext.connected = true + DebuggerEvents.emit('connect') + DebuggerEvents.emit('*', 'connect') + }) + + ws.addEventListener('close', () => { + DebuggerContext.connected = false + DebuggerEvents.emit('disconnect') + DebuggerEvents.emit('*', 'disconnect') + }) + + ws.addEventListener('error', e => { + DebuggerContext.connected = false + DebuggerEvents.emit('error', e) + DebuggerEvents.emit('*', 'error', e) + }) + + ws.addEventListener('message', e => { + try { + const json = JSON.parse(e.data) as { + code: string + nonce: string + } + + if (typeof json.code === 'string' && typeof json.nonce === 'string') { + let res: unknown + try { + // biome-ignore lint/security/noGlobalEval: This is intentional + res = globalThis.eval(json.code) + } catch (e) { + res = e + } + + const inspect = + revenge.modules.findProp< + (val: unknown, opts?: { depth?: number; showHidden?: boolean; color?: boolean }) => string + >('inspect')! + + try { + ws.send( + res instanceof Error + ? JSON.stringify({ + level: 'error', + message: String(res), + nonce: json.nonce, + }) + : JSON.stringify({ + level: 'info', + message: inspect(res, { showHidden: true }), + nonce: json.nonce, + }), + ) + } catch (e) { + ws.send( + JSON.stringify({ + level: 'error', + message: `DebuggerError: ${String(e)}`, + nonce: json.nonce, + }), + ) + } + } + } catch {} + }) +} diff --git a/src/plugins/developer-settings/index.tsx b/src/plugins/developer-settings/index.tsx index c3d3393..bdbf8a1 100644 --- a/src/plugins/developer-settings/index.tsx +++ b/src/plugins/developer-settings/index.tsx @@ -1,3 +1,5 @@ +/// + import { toasts } from '@revenge-mod/modules/common' import { registerPlugin } from '@revenge-mod/plugins/internals' import { sleep } from '@revenge-mod/utils/functions' @@ -6,16 +8,22 @@ import AssetBrowserSettingsPage from './pages/AssetBrowser' import DebugPerformanceTimesSettingsPage from './pages/DebugPerformanceTimes' import DeveloperSettingsPage from './pages/Developer' +import { connectToDebugger, DebuggerContext } from './debugger' import { DevToolsEvents, connectToDevTools } from './devtools' import type { PluginContextFor } from '@revenge-mod/plugins' import type { FunctionComponent } from 'react' +import { BundleUpdaterManager } from '@revenge-mod/modules/native' const plugin = registerPlugin<{ reactDevTools: { address: string autoConnect: boolean } + debugger: { + address: string + autoConnect: boolean + } }>( { name: 'Developer Settings', @@ -58,6 +66,9 @@ const plugin = registerPlugin<{ if (storage.reactDevTools.autoConnect && globalThis.__reactDevTools) connectToDevTools(storage.reactDevTools.address) + if (storage.debugger.autoConnect) connectToDebugger(storage.debugger.address, context.revenge) + + setupDebugger(context) // Wait for the section to be added by the Settings plugin await sleep(0) @@ -92,10 +103,62 @@ const plugin = registerPlugin<{ address: 'localhost:8097', autoConnect: false, }, + debugger: { + address: 'localhost:9090', + autoConnect: false, + }, }), }, true, true, ) +function setupDebugger({ patcher, cleanup }: PluginContextFor) { + const debuggerCleanups = new Set<() => unknown>() + + patcher.before( + globalThis, + 'nativeLoggingHook', + ([message, level]) => { + if (DebuggerContext.ws?.readyState === WebSocket.OPEN) + DebuggerContext.ws.send( + JSON.stringify({ + level: level === 3 ? 'error' : level === 2 ? 'warn' : 'info', + message, + }), + ) + }, + 'loggerPatch', + ) + + globalThis.dbgr = { + reload: () => BundleUpdaterManager.reload(), + patcher: { + snipe: (object, key, callback) => + debuggerCleanups.add( + patcher.after( + object, + key, + callback ?? ((args, ret) => console.log('[SNIPER]', args, ret)), + 'revenge.plugins.developer-settings.debugger.patcher.snipe', + ), + ), + noop: (object, key) => + debuggerCleanups.add(patcher.instead(object, key, () => void 0, 'revenge.plugins.developer-settings.debugger.patcher.noop')), + wipe: () => { + for (const c of debuggerCleanups) c() + debuggerCleanups.clear() + }, + }, + } + + cleanup( + // biome-ignore lint/performance/noDelete: This happens once + () => delete globalThis.dbgr, + () => { + for (const c of debuggerCleanups) c() + }, + ) +} + export const PluginContext = React.createContext>(null!) diff --git a/src/plugins/developer-settings/pages/Developer.tsx b/src/plugins/developer-settings/pages/Developer.tsx index 8ba3a7a..c15fc90 100644 --- a/src/plugins/developer-settings/pages/Developer.tsx +++ b/src/plugins/developer-settings/pages/Developer.tsx @@ -22,6 +22,13 @@ import { connectToDevTools, disconnectFromDevTools, } from '../devtools' +import { + connectToDebugger, + DebuggerContext, + DebuggerEvents, + disconnectFromDebugger, + type DebuggerEventsListeners, +} from '../debugger' import { settings } from '@revenge-mod/preferences' import { ScrollView } from 'react-native' @@ -41,12 +48,15 @@ export default function DeveloperSettingsPage() { const navigation = NavigationNative.useNavigation() const refDevToolsAddr = useRef(storage.reactDevTools.address || 'localhost:8097') - const [connected, setConnected] = useState(DevToolsContext.connected) + const [rdtConnected, setRdtConnected] = useState(DevToolsContext.connected) + + const refDebuggerAddr = useRef(storage.debugger.address || 'localhost:9090') + const [dbgConnected, setDbgConnected] = useState(DebuggerContext.connected) useEffect(() => { const listener: DevToolsEventsListeners['*'] = evt => { - if (evt === 'connect') setConnected(true) - else setConnected(false) + if (evt === 'connect') setRdtConnected(true) + else setRdtConnected(false) } DevToolsEvents.on('*', listener) @@ -54,60 +64,115 @@ export default function DeveloperSettingsPage() { return () => void DevToolsEvents.off('*', listener) }, []) + useEffect(() => { + const listener: DebuggerEventsListeners['*'] = evt => { + if (evt === 'connect') setDbgConnected(true) + else setDbgConnected(false) + } + + DebuggerEvents.on('*', listener) + + return () => void DebuggerEvents.off('*', listener) + }, []) + return ( - {typeof __reactDevTools !== 'undefined' && ( - - (refDevToolsAddr.current = text)} - onBlur={() => { - if (refDevToolsAddr.current === storage.reactDevTools.address) return - storage.reactDevTools.address = refDevToolsAddr.current - - toasts.open({ - key: 'revenge.plugins.settings.react-devtools.saved', - content: 'Saved DevTools address!', - }) - }} - returnKeyType="done" - /> - {/* Rerender when connected changes */} - - {connected ? ( - - } - onPress={() => disconnectFromDevTools()} - /> - ) : ( - + {typeof __reactDevTools !== 'undefined' && ( + <> + (refDevToolsAddr.current = text)} + onBlur={() => { + if (refDevToolsAddr.current === storage.reactDevTools.address) return + storage.reactDevTools.address = refDevToolsAddr.current + + toasts.open({ + key: 'revenge.plugins.settings.react-devtools.saved', + content: 'Saved DevTools address!', + }) + }} + returnKeyType="done" + /> + {/* Rerender when connected changes */} + + {rdtConnected ? ( + + } + onPress={() => disconnectFromDevTools()} + /> + ) : ( + } + onPress={() => connectToDevTools(refDevToolsAddr.current)} + /> + )} + } - onPress={() => connectToDevTools(refDevToolsAddr.current)} + value={storage.reactDevTools.autoConnect} + onValueChange={v => (storage.reactDevTools.autoConnect = v)} /> - )} - } - value={storage.reactDevTools.autoConnect} - onValueChange={v => (storage.reactDevTools.autoConnect = v)} + + + )} + (refDebuggerAddr.current = text)} + onBlur={() => { + if (refDebuggerAddr.current === storage.debugger.address) return + storage.debugger.address = refDebuggerAddr.current + + toasts.open({ + key: 'revenge.plugins.developer-settings.debugger.saved', + content: 'Saved debugger address!', + }) + }} + returnKeyType="done" + /> + {/* Rerender when connected changes */} + + {dbgConnected ? ( + } + onPress={() => disconnectFromDebugger()} /> - - - )} + ) : ( + } + onPress={() => connectToDebugger(storage.debugger.address, context.revenge)} + /> + )} + } + value={storage.debugger.autoConnect} + onValueChange={v => (storage.debugger.autoConnect = v)} + /> + + unknown, timeout?: number): number /** * Calls the garbage collector