Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add debugger #8

Merged
merged 5 commits into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified bun.lockb
Binary file not shown.
12 changes: 11 additions & 1 deletion libraries/modules/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,17 @@ export namespace DiscordModules {
}

// Other
export type Slider = FC
export type Slider = FC<{
value: number
step: number
minimumValue: number
maximumValue: number
onValueChange?: (value: number) => void
onSlidingStart?: () => void
onSlidingComplete?: () => void
startIcon?: ReactNode
endIcon?: ReactNode
}>
export type FlashList = FC
export type Text = FC<
TextProps & {
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"license": "BSD-3-Clause",
"scripts": {
"serve": "bun run ./scripts/serve.mjs",
"build": "bun run ./scripts/build.mjs"
"build": "bun run ./scripts/build.mjs",
"debugger": "node ./scripts/debugger.mjs"
},
"dependencies": {
"@revenge-mod/app": "workspace:*",
Expand All @@ -26,10 +27,12 @@
"@swc/helpers": "^0.5.15",
"@tsconfig/strictest": "^2.0.5",
"chalk": "^5.3.0",
"clipboardy": "^4.0.0",
"esbuild": "^0.24.0",
"esbuild-plugin-globals": "^0.2.0",
"react-native": "^0.76.3",
"typescript": "^5.7.2",
"ws": "^8.18.0",
"yargs-parser": "^21.1.1"
},
"workspaces": ["libraries/*"],
Expand Down
147 changes: 147 additions & 0 deletions scripts/debugger.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
#!/usr/bin/env node
import * as repl from 'node:repl'
import { WebSocketServer } from 'ws'
import os from 'os'
import chalk from 'chalk'
import clipboardy from 'clipboardy'
import { join, resolve } from 'path'
import { existsSync } from 'fs'
import { mkdir, writeFile } from 'fs/promises'

const debuggerHistoryPath = resolve(join('node_modules', 'debugger'))

if ('Bun' in globalThis)
throw new Error(
`Bun is unsupported due to it lacking compatibility with node:repl. Please run "node ./scripts/debugger.mjs" or "bun debugger"`,
)

let isPrompting = false

const debuggerColorify = message => (isPrompting ? '\n' : '') + chalk.bold.blue('[Debugger] ') + message

const clientColorify = (style, message) =>
(isPrompting ? '\n' : '') +
(style === 'error'
? chalk.bold.red('[Revenge] ERR! ') + chalk.red(message)
: style === 'warn'
? chalk.bold.yellow('[Revenge] ') + chalk.yellow(message)
: chalk.bold.green('[Revenge] ') + message)

const logAsDebugger = message => console.info(debuggerColorify(message))

const logAsClient = message => 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 = '--clear'

export function serve() {
let websocketOpen = false
let nextReply

const wss = new WebSocketServer({
port: 9090,
})
wss.on('connection', ws => {
if (websocketOpen) return
websocketOpen = true

logAsDebugger('Client connected')

ws.on('message', data => {
try {
/** @type {{ level: "info" | "warn" | "error", message: string, nonce?: string }} */
const json = JSON.parse(data.toString())

if (nextReply?.cb && nextReply?.nonce && nextReply.nonce === json.nonce) {
if (json.level === 'info' && nextReply.toCopy) {
clipboardy.write(json.message)
nextReply.cb(null, debuggerColorify('Copied result to clipboard'))
} else
nextReply.cb(
null,
json.level === 'error'
? clientColorify('error', json.message)
: clientColorify(null, json.message),
)
nextReply = null
isPrompting = true
} else {
if (json.level === 'error') logAsClientError(json.message)
else if (json.level === 'warn') logAsClientWarn(json.message)
else logAsClient(json.message)

if (isPrompting) rl.displayPrompt(true)
}
} catch {}
})

isPrompting = true
const rl = repl.start({
eval(input, _, __, cb) {
if (!isPrompting) return
if (!input.trim()) return cb()

try {
isPrompting = false

const code = input.trim()
if (code === clearHistoryPrompt) {
writeFile(join(debuggerHistoryPath, 'history.txt'), '')
logAsDebugger('Cleared REPL history')
return cb()
}

nextReply = {
nonce: crypto.randomUUID(),
cb,
toCopy: code.endsWith(copyPrompt),
}
ws.send(
JSON.stringify({
code: code.endsWith(copyPrompt) ? code.slice(0, -copyPrompt.length) : code,
nonce: nextReply.nonce,
}),
)
} catch (e) {
cb(e)
}
},
writer(msg) {
return msg
},
})
rl.setupHistory(join(debuggerHistoryPath, 'history.txt'), () => void 0)

rl.on('close', () => {
isPrompting = false
ws.close()
})

ws.on('close', () => {
logAsDebugger('Client disconnected')
rl.close()
websocketOpen = false
})
})

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
}

if (!existsSync(debuggerHistoryPath)) await mkdir(debuggerHistoryPath, { recursive: true })

serve()
14 changes: 14 additions & 0 deletions src/plugins/developer-settings/debugger.d.ts
Original file line number Diff line number Diff line change
@@ -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 {}
96 changes: 96 additions & 0 deletions src/plugins/developer-settings/debugger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { EventEmitter } from 'events'
import type { RevengeLibrary } from '@revenge-mod/revenge'

export const DebuggerEvents = new EventEmitter<DebuggerEventsListeners>()

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 {}
})
}
Loading
Loading