diff --git a/demo/main.js b/demo/main.js index a5e3661..9d7ee35 100644 --- a/demo/main.js +++ b/demo/main.js @@ -2,4 +2,4 @@ console.log("Started"); const now = new Date(); require("C:\\Users\\henni\\AppData\\Local\\Yarn\\Data\\global\\node_modules\\easy-attach\\")(); const dayOfWeek = now.getDayOfWeek(); -console.log("Day of week:", dayOfWeek); \ No newline at end of file +console.log("Day of week:", dayOfWeek); diff --git a/entries/index.d.ts b/entries/index.d.ts new file mode 100644 index 0000000..42cbb91 --- /dev/null +++ b/entries/index.d.ts @@ -0,0 +1,4 @@ +import { EasyAttachArgs } from "../dist/lib"; + +declare const launchDebugger: (args: EasyAttachArgs) => void; +export = launchDebugger; diff --git a/entries/index.js b/entries/index.js index cb671c4..b601220 100644 --- a/entries/index.js +++ b/entries/index.js @@ -7,6 +7,5 @@ function randomInt(min, max) { const randomIndex = randomInt(0, entries.length - 1); // choose a random quote. -/** @type {(arg: import("../src/lib").EasyAttachArgs) => void} */ const entry = entries[randomIndex]; module.exports = entry; diff --git a/package.json b/package.json index a24d9bf..9ac47a6 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,13 @@ { "name": "easy-attach", - "version": "0.15.3", + "version": "0.16.0", "author": { "name": "Henning Dieterichs" }, "description": "A helper tool that makes launching the debugger to step through obscure node-js scripts (e.g. webpack configurations) extremely easy.", "homepage": "https://github.com/hediet/easy-attach", "main": "./entries/index.js", + "types": "./entries/index.d.ts", "license": "MIT", "bin": { "easy-attach": "./dist/cli.entry.js" diff --git a/src/background-worker/entry.ts b/src/background-worker/entry.ts index e84683b..df62b4b 100644 --- a/src/background-worker/entry.ts +++ b/src/background-worker/entry.ts @@ -4,12 +4,15 @@ import { AttachContext, Result } from "./attachContext"; import { EventSource } from "@hediet/std/events"; import { Disposable, dispose } from "@hediet/std/disposable"; import { launchProxyServer } from "../debugger-proxy"; +import { BackgroundWorkerArgs } from "."; -const args = process.argv.slice(2); -const debuggerPort = parseInt(args[0]); -const label = args.length > 1 ? args[1] : undefined; +const argsObj = JSON.parse(process.argv[2]) as BackgroundWorkerArgs; -launchProxyServer(debuggerPort).then(data => { +launchProxyServer({ + debugPort: argsObj.debugPort, + eagerExit: argsObj.eagerExitDebugProxy, + debugProxyPortConfig: argsObj.debugProxyPortConfig, +}).then(data => { launch(data.port, data.onClientConnected, data.signalExit); }); @@ -30,13 +33,13 @@ async function launch( }); const context: AttachContext = { - debuggerPort, + debuggerPort: argsObj.debugPort, proxyPort, disposables, exit, - label, + label: argsObj.label, log: (message: string) => { - if (process.env.DEBUG_EASY_ATTACH) { + if (process.env.DEBUG_EASY_ATTACH || argsObj.log) { console.log(message); } }, diff --git a/src/background-worker/index.ts b/src/background-worker/index.ts index 46cc973..3481867 100644 --- a/src/background-worker/index.ts +++ b/src/background-worker/index.ts @@ -1,14 +1,22 @@ import { join } from "path"; import { spawnSync } from "child_process"; +import { PortConfig } from "../lib"; + +export interface BackgroundWorkerArgs { + debugPort: number; + label: string | undefined; + eagerExitDebugProxy: boolean; + log: boolean; + debugProxyPortConfig: PortConfig; +} export function launchAndWaitForBackgroundProcessSync( - debugPort: number, - label: string | undefined + args: BackgroundWorkerArgs ) { const entry = join(__dirname, "./entry.js"); - spawnSync( - "node", - [`${entry}`, debugPort.toString(), label ? JSON.stringify(label) : ""], - { stdio: "inherit", shell: true, windowsHide: true } - ); + spawnSync("node", [`${entry}`, JSON.stringify(args)], { + stdio: "inherit", + shell: false, + windowsHide: true, + }); } diff --git a/src/cli.entry.ts b/src/cli.entry.ts index 6d44ae7..0c3dd93 100644 --- a/src/cli.entry.ts +++ b/src/cli.entry.ts @@ -12,7 +12,12 @@ console.log(`Easy Attach Version ${pkg.version}.`); console.log(); console.log("Use this code to trigger a breakpoint:"); console.log(chalk.blue(codeToTriggerDebugger)); -console.log("(Pasted into your clipboard)"); -console.log(); -clipboardy.writeSync(codeToTriggerDebugger); +try { + clipboardy.writeSync(codeToTriggerDebugger); + console.log("(Copied to clipboard)"); +} catch (e) { + console.error(chalk.red(`Could not copy to clipboard: ${e.toString()}`)); +} + +console.log(); diff --git a/src/debugger-proxy/entry.ts b/src/debugger-proxy/entry.ts index 6b1478d..50d2006 100644 --- a/src/debugger-proxy/entry.ts +++ b/src/debugger-proxy/entry.ts @@ -2,12 +2,12 @@ import { AddressInfo } from "net"; import { debuggerProxyContract } from "./contract"; import httpProxy = require("http-proxy"); import WsParser = require("simples/lib/parsers/ws"); -import { TypedChannel } from "@hediet/typed-json-rpc"; import { NodeJsMessageStream } from "@hediet/typed-json-rpc-streams"; import { ResettableTimeout } from "@hediet/std/timer"; +import { DebuggerProxyArgs } from "."; +import getPort from "get-port"; -const args = process.argv.slice(2); -const debuggerPort = parseInt(args[0]); +const argsObj = JSON.parse(process.argv[2]) as DebuggerProxyArgs; let clientConnected = false; const timeout = new ResettableTimeout(5000); @@ -28,20 +28,30 @@ const { client } = debuggerProxyContract.registerServerToStream( } ); +function handleClientConnected() { + if (!clientConnected) { + clientConnected = true; + client.clientConnected({}); + } +} + const server = httpProxy.createServer({ - target: `http://localhost:${debuggerPort}`, + target: `http://localhost:${argsObj.debugPort}`, ws: true, }); server.on("proxyReqWs", (proxyReq, req, socket, options) => { + if (argsObj.eagerExit) { + handleClientConnected(); + } + const parser = new WsParser(0, false); socket.pipe(parser); parser.on("frame", (frame: { data: any }) => { const content = frame.data.toString("utf8") as string; if (content.indexOf("Runtime.runIfWaitingForDebugger") !== -1) { - clientConnected = true; - client.clientConnected({}); + handleClientConnected(); } }); @@ -62,7 +72,18 @@ server._server.on("connection", socket => { }); */ -server.listen(0); +async function run() { + let port: number; + if (argsObj.debugProxyPortConfig === "random") { + port = 0; + } else { + port = await getPort({ port: argsObj.debugProxyPortConfig }); + } + + server.listen(port); + + const info = (server as any)._server.address() as AddressInfo; + client.serverStarted({ port: info.port }); +} -const info = (server as any)._server.address() as AddressInfo; -client.serverStarted({ port: info.port }); +run(); diff --git a/src/debugger-proxy/index.ts b/src/debugger-proxy/index.ts index a7a85ae..6edc589 100644 --- a/src/debugger-proxy/index.ts +++ b/src/debugger-proxy/index.ts @@ -1,14 +1,20 @@ import { spawn } from "child_process"; import { join } from "path"; import { EventEmitter, EventSource } from "@hediet/std/events"; -import { TypedChannel } from "@hediet/typed-json-rpc"; import { NodeJsMessageStream } from "@hediet/typed-json-rpc-streams"; import { debuggerProxyContract } from "./contract"; import { startInterval } from "@hediet/std/timer"; import { Disposable } from "@hediet/std/disposable"; +import { PortConfig } from "../lib"; + +export interface DebuggerProxyArgs { + debugPort: number; + debugProxyPortConfig: PortConfig; + eagerExit: boolean; +} export function launchProxyServer( - port: number + args: DebuggerProxyArgs ): Promise<{ port: number; onClientConnected: EventSource; @@ -17,7 +23,7 @@ export function launchProxyServer( const onClientConnected = new EventEmitter(); return new Promise(resolve => { const entry = join(__dirname, "./entry.js"); - const proc = spawn("node", [entry, port.toString()], { + const proc = spawn("node", [entry, JSON.stringify(args)], { detached: true, shell: false, windowsHide: true, diff --git a/src/lib.ts b/src/lib.ts index 6749eeb..413b04c 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -1,9 +1,36 @@ import child_process = require("child_process"); import { launchAndWaitForBackgroundProcessSync } from "./background-worker"; +export type PortConfig = "random" | number | number[]; +export type DebugPortConfig = PortConfig | "preconfigured"; + export interface EasyAttachArgs { - label: string; - continue: boolean; + /** + * Sets a label for the debug target. + */ + label?: string; + /** + * If enabled, it does not break after attaching the debugger. + */ + continue?: boolean; + /** + * Specifies the port to use for the debug port. + * Use `preconfigured` when the debugger was already launched. + */ + debugPort?: DebugPortConfig; + /** + * Specifies the port to use for the debug proxy. + * This is usefull if you want to forward this port. + */ + debugProxyPort?: PortConfig; + /** + * Use this option when the debug proxy does not recognize connection attempts. + */ + eagerExitDebugProxy?: boolean; + /** + * Print logs from background worker. + */ + logBackgroundWorker?: boolean; } let first = true; @@ -13,9 +40,35 @@ module.exports.debugProcessAndWait = function(args?: EasyAttachArgs): boolean { } first = false; - const label = args ? args.label : undefined; - const { debugPort } = initializeDebugPort(); - launchAndWaitForBackgroundProcessSync(debugPort, label); + let label = undefined; + let debugPortConfig: DebugPortConfig = "random"; + let debugProxyPortConfig: PortConfig = "random"; + let eagerExitDebugProxy = false; + let log = false; + + if (args) { + label = args.label; + debugPortConfig = args.debugPort || "random"; + if (args.eagerExitDebugProxy !== undefined) { + eagerExitDebugProxy = args.eagerExitDebugProxy; + } + if (args.logBackgroundWorker !== undefined) { + log = args.logBackgroundWorker; + } + + if (args.debugProxyPort) { + debugProxyPortConfig = args.debugProxyPort; + } + } + + const { debugPort } = initializeDebugPort(debugPortConfig); + launchAndWaitForBackgroundProcessSync({ + debugPort, + label, + log, + eagerExitDebugProxy, + debugProxyPortConfig, + }); // Wait a bit so that the dev tools can connect properly. waitSomeCycles(); @@ -29,24 +82,45 @@ module.exports.debugProcessAndWait = function(args?: EasyAttachArgs): boolean { let debugPort: number | undefined = undefined; -function initializeDebugPort(): { debugPort: number } { +function initializeDebugPort( + portConfig: DebugPortConfig +): { debugPort: number } { // use a random port for debugPort to prevent port clashes. if (!debugPort) { - debugPort = getRandomPortSync(); - process.debugPort = debugPort; + if (portConfig === "preconfigured") { + debugPort = process.debugPort; + } else { + if (portConfig === "random") { + debugPort = getRandomPortSync(); + } else if (typeof portConfig === "number") { + debugPort = portConfig; + } else { + debugPort = getRandomPortSync(portConfig); + } + process.debugPort = debugPort; + } + (process as any)._debugProcess(process.pid); } return { debugPort }; } -function getRandomPortSync(): number { +function getRandomPortSync(allowedPorts?: number[]): number { + let options = ""; + if (allowedPorts !== undefined) { + options = `{ port: [${allowedPorts + .map(p => (+p).toString()) + .join(",")}] }`; + } // we must use execSync as get-port is async. const portStr = child_process.execSync( - `node -e "require('get-port')().then(p => console.log(p))"`, + `node -e "require('get-port')(${options}).then(p => console.log(p))"`, // Set cwd so that node_modules can be found. { cwd: __dirname, encoding: "utf8" } ); - return parseInt(portStr); + const port = parseInt(portStr); + // It could be that `port` is not from `allowedPorts` as they are only preferences. + return port; } function waitSomeCycles() { diff --git a/tsconfig.json b/tsconfig.json index 435c07c..47bf55d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "skipLibCheck": true, "rootDir": "./src", "resolveJsonModule": true, + "declaration": true, "newLine": "LF" }, "include": ["./src/**/*"]