-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
453 additions
and
53 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} | ||
}) | ||
} |
Oops, something went wrong.