├── .gitignore ├── tslint.json ├── tsconfig.json ├── src ├── nr-types.ts ├── locales │ ├── ja │ │ └── flow-debugger.json │ └── en-US │ │ └── flow-debugger.json ├── lib │ ├── types.ts │ ├── location.ts │ ├── MessageQueue.ts │ └── debugger.ts ├── flow-debugger.ts ├── style.css └── flow-debugger.html ├── CHANGELOG.md ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ └── bug_report.yml └── workflows │ └── tests.yml ├── package.json ├── scripts └── copy-static-assets.js ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | resources 2 | node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": {}, 7 | "rules": { 8 | "no-console": false 9 | }, 10 | "rulesDirectory": [] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "esModuleInterop": true, 5 | "target": "es6", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "outDir": "dist", 9 | "forceConsistentCasingInFileNames": true 10 | }, 11 | "lib": ["es2015"], 12 | "include": ["./src/**/*"] 13 | } 14 | -------------------------------------------------------------------------------- /src/nr-types.ts: -------------------------------------------------------------------------------- 1 | export type SendEvent = { 2 | msg: any, 3 | source: { 4 | id: string, 5 | node: any, 6 | port: number 7 | }, 8 | destination: { 9 | id: string, 10 | node: any, 11 | }, 12 | cloneMessage: boolean 13 | } 14 | 15 | export type ReceiveEvent = { 16 | msg: any, 17 | destination: { 18 | id: string, 19 | node: any, 20 | } 21 | } 22 | 23 | export type EventCallback = (error?: any) => any; 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 1.1.1 2 | 3 | - Do not send full message object in messageQueued event Fixes #8 4 | 5 | ### 1.1.0 6 | 7 | - Require Node-RED 2.0.0-beta.2 8 | - Allow debugger to pause just breakpoint nodes 9 | - Better display >99 message queue count on node annotation 10 | - Publish debugger state as retained to make resync easier 11 | - Add Japanese translations (#7) @kazuhitoyokoi 12 | 13 | ### 1.0.1 14 | 15 | - Fix case sensitivity of filenames due to TypeScript being TypeScript 16 | 17 | ### 1.0.0 18 | 19 | - First public release of the Flow Debugger 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: ❓ Questions 4 | url: https://discourse.nodered.org 5 | about: Ask your question on the Node-RED forum 6 | - name: ⭐️ Feature Request 7 | url: https://discourse.nodered.org/c/development/feature-requests 8 | about: Discuss your request with the community 9 | - name: 🗂 Documentation 10 | url: https://nodered.org/docs 11 | about: Go straight to the documentation 12 | - name: 💬 Slack 13 | url: https://nodered.org/slack 14 | about: Chat about the project on our slack team 15 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Run tests 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions/setup-node@v2 18 | with: 19 | node-version: 14 20 | - name: Install Dependencies 21 | run: npm install 22 | - name: Run tests 23 | run: npm run test 24 | -------------------------------------------------------------------------------- /src/locales/ja/flow-debugger.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": { 3 | "debugger": "フローデバッガ", 4 | "debuggerShort": "デバッガ", 5 | "breakpoints": "ブレイクポイント", 6 | "messages": "メッセージ", 7 | "output": "出力", 8 | "input": "入力", 9 | "paused": "フローを停止しました", 10 | "resume": "フローを再開", 11 | "step": "フローをステップ実行", 12 | "pause": "フローを停止", 13 | "deleteMessage": "メッセージを削除", 14 | "stepMessage": "メッセージをステップ実行", 15 | "filter": { 16 | "label": "メッセージの絞り込み", 17 | "all": "全てのノード", 18 | "flow": "現在のフロー" 19 | }, 20 | "settings": "デバッガオプション", 21 | "breakpointAction": { 22 | "label": "ブレイクポイントの動作", 23 | "pause-all": "全てのノードを停止", 24 | "pause-bp": "ブレイクポイントで停止" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/locales/en-US/flow-debugger.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": { 3 | "debugger": "Flow Debugger", 4 | "debuggerShort": "debugger", 5 | "breakpoints": "Breakpoints", 6 | "messages":"Messages", 7 | "output": "output", 8 | "input": "input", 9 | "paused": "Flows paused", 10 | "resume": "Resume flows", 11 | "step": "Step flows", 12 | "pause": "Pause flows", 13 | "deleteMessage": "Delete message", 14 | "stepMessage": "Step message", 15 | "filter": { 16 | "label": "Filter messages", 17 | "all": "all nodes", 18 | "flow": "current flow" 19 | }, 20 | "settings": "Debugger options", 21 | "breakpointAction": { 22 | "label": "Breakpoint action", 23 | "pause-all": "pause all nodes", 24 | "pause-bp": "pause at breakpoint" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import {ReceiveEvent, SendEvent, EventCallback} from "../nr-types" 2 | import * as Location from "./location" 3 | 4 | export interface MessageEvent { 5 | id: number; 6 | event: SendEvent | ReceiveEvent; 7 | location: Location.Location; 8 | done: EventCallback; 9 | nextByLocation: MessageEvent; 10 | previousByLocation: MessageEvent; 11 | nextByTime: MessageEvent; 12 | previousByTime: MessageEvent; 13 | } 14 | 15 | /** 16 | * Triggered when the debugger is paused 17 | * @param reason why the debugger paused: 'breakpoint', 'step', 'manual' 18 | * @param node the id of the node that is paused 19 | * @param breakpoint the breakpoint, if any, that triggered the pause 20 | * @param data any other data associated with the event 21 | */ 22 | export interface PausedEvent { 23 | reason: string, 24 | node?: string, 25 | breakpoint?: string, 26 | pausedLocations?: string[], 27 | data?: any 28 | } 29 | 30 | export interface Breakpoint { 31 | id: string, 32 | location: Location.Location, 33 | active: boolean, 34 | mode: "all" | "flow" | "node" 35 | } 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-red-debugger", 3 | "version": "1.1.1", 4 | "description": "A flow debugger for Node-RED 2.x", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/node-red/node-red-debugger.git" 8 | }, 9 | "scripts": { 10 | "build": "(tsc || exit 1) && npm run tslint && npm run copyAssets", 11 | "tslint": "tslint -c tslint.json -p tsconfig.json", 12 | "copyAssets": "node scripts/copy-static-assets.js", 13 | "dev": "nodemon --exec 'npm run build' -i dist -i resources -e 'ts html css'", 14 | "test": "npm run build" 15 | }, 16 | "keywords": ["node-red","debugger"], 17 | "author": "Nick O'Leary ", 18 | "files": [ 19 | "dist", 20 | "resources" 21 | ], 22 | "license": "Apache-2", 23 | "node-red": { 24 | "version": ">=2.0.0", 25 | "plugins": { 26 | "flow-debugger": "dist/flow-debugger.js" 27 | } 28 | }, 29 | "devDependencies": { 30 | "@types/node": "^15.12.2", 31 | "fs-extra": "^10.0.0", 32 | "html-minifier": "^4.0.0", 33 | "nodemon": "^2.0.7", 34 | "tslint": "^6.1.3", 35 | "typescript": "^4.3.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/lib/location.ts: -------------------------------------------------------------------------------- 1 | import {ReceiveEvent, SendEvent} from "../nr-types" 2 | 3 | type PortType = "i" | "o"; 4 | 5 | export class Location { 6 | /** The node id. This could be a generated node id from inside a subflow */ 7 | id: string; 8 | /** The path to the node. This will be resolvable to an actual node in the flow */ 9 | path: string; 10 | /** The type of port - either 'o' or 'i' */ 11 | portType: PortType; 12 | /** The index of the port - 0-indexed */ 13 | portIndex: number; 14 | 15 | inSubflow:boolean = false; 16 | 17 | constructor(nodeId:string, nodePath:string, portType:PortType="o", portIndex=0) { 18 | this.id = nodeId; 19 | this.path = nodePath; 20 | this.portType = portType; 21 | this.portIndex = portIndex; 22 | } 23 | getBreakpointLocation(): string { 24 | if (this.inSubflow) { 25 | return `*/${this.id}[${this.portType}][${this.portIndex}]` 26 | } else { 27 | return this.toString(); 28 | } 29 | } 30 | toString(): string { 31 | return `${this.path}/${this.id}[${this.portType}][${this.portIndex}]` 32 | } 33 | } 34 | 35 | export function createLocation(event:ReceiveEvent|SendEvent): Location { 36 | let node:any; 37 | let portType:PortType; 38 | let portIndex:number; 39 | if (event.hasOwnProperty("source")) { 40 | node = (event as SendEvent).source.node; 41 | portType = "o"; 42 | portIndex = (event as SendEvent).source.port; 43 | } else { 44 | node = (event as ReceiveEvent).destination.node; 45 | portType = "i"; 46 | portIndex = 0; 47 | } 48 | const l = new Location(node._alias || node.id, node._flow.path, portType, portIndex); 49 | if (node._alias) { 50 | l.inSubflow = true; 51 | } 52 | return l; 53 | } 54 | -------------------------------------------------------------------------------- /scripts/copy-static-assets.js: -------------------------------------------------------------------------------- 1 | const minify = require("html-minifier").minify; 2 | const fs = require("fs-extra"); 3 | const path = require("path"); 4 | 5 | 6 | const projectRoot = path.join(__dirname,"..") 7 | const resources = path.join(projectRoot,"resources"); 8 | const dist = path.join(projectRoot,"dist"); 9 | const src = path.join(projectRoot,"src"); 10 | 11 | const assets = {} 12 | assets[dist] = [ 13 | "flow-debugger.html", 14 | "locales" 15 | ] 16 | assets[resources] = [ 17 | "style.css" 18 | ] 19 | 20 | async function copyStaticAssets(dist,assets) { 21 | await fs.mkdir(dist,{recursive: true}); 22 | for (let i=0; i"+rawCSS+"", {minifyCSS: true}); 31 | const finalCSS = minifiedCSS.substring(7,minifiedCSS.length-8) 32 | await fs.writeFile(path.join(dist,assets[i]), finalCSS) 33 | } else { 34 | await fs.mkdir(path.join(dist,assets[i]), {recursive: true}); 35 | await fs.copy(path.join(src,assets[i]),path.join(dist,assets[i])) 36 | } 37 | } 38 | } 39 | 40 | 41 | 42 | (async function() { 43 | const destinations = Object.keys(assets); 44 | for (let i=0, l=destinations.length; i { 48 | console.error(err); 49 | process.exit(1); 50 | }); 51 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Report a bug 2 | description: File a bug/issue on node-red-debugger 3 | labels: [needs-triage] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | This issue tracker is for problems with the Node-RED Flow Debugger 9 | 10 | If your issue is: 11 | - a general 'how-to' type question, 12 | - a feature request or suggestion for a change, 13 | - or problems with 3rd party (`node-red-contrib-`) nodes 14 | 15 | please use the [Node-RED Forum](https://discourse.nodered.org) or [slack team](https://nodered.org/slack). 16 | 17 | You could also consider asking a question on [Stack Overflow](https://stackoverflow.com/questions/tagged/node-red) and tag it `node-red`. 18 | 19 | That way the whole Node-RED user community can help, rather than rely on the core development team. 20 | 21 | To help us understand the issue, please fill-in as much of the following information as you can: 22 | - type: textarea 23 | attributes: 24 | label: Current Behavior 25 | description: A clear & concise description of what you're experiencing. 26 | validations: 27 | required: false 28 | - type: textarea 29 | attributes: 30 | label: Expected Behavior 31 | description: A clear & concise description of what you expected to happen. 32 | validations: 33 | required: false 34 | - type: textarea 35 | attributes: 36 | label: Steps To Reproduce 37 | description: Steps to reproduce the behavior. 38 | validations: 39 | required: false 40 | - type: textarea 41 | attributes: 42 | label: Example flow 43 | description: If you have a minimal example flow that demonstrates the issue, share it here. 44 | value: | 45 | ``` 46 | paste your flow here 47 | ``` 48 | validations: 49 | required: false 50 | - type: textarea 51 | attributes: 52 | label: Environment 53 | description: Please tell us about your environment. Include any relevant information on how you are running Node-RED. 54 | value: | 55 | - node-red-debugger version: 56 | - Node-RED version: 57 | - Node.js version: 58 | - npm version: 59 | - Platform/OS: 60 | - Browser: 61 | validations: 62 | required: false 63 | -------------------------------------------------------------------------------- /src/lib/MessageQueue.ts: -------------------------------------------------------------------------------- 1 | import {MessageEvent} from "./types" 2 | 3 | export class MessageQueue { 4 | queueName: string; 5 | private previousName: string; 6 | private nextName: string; 7 | head: MessageEvent; 8 | tail: MessageEvent; 9 | length: number; 10 | 11 | constructor(queueName:string) { 12 | this.queueName = queueName; 13 | this.previousName = `previousBy${queueName}` 14 | this.nextName = `nextBy${queueName}` 15 | this.length = 0; 16 | } 17 | enqueue(event:MessageEvent) { 18 | if (!this.head) { 19 | this.head = event; 20 | } 21 | event[this.previousName] = this.tail; 22 | if (this.tail) { 23 | this.tail[this.nextName] = event; 24 | } 25 | this.tail = event; 26 | this.length++; 27 | } 28 | next():MessageEvent { 29 | const result = this.head; 30 | if (result) { 31 | this.remove(result); 32 | this.length--; 33 | } 34 | return result; 35 | } 36 | peek():MessageEvent { 37 | return this.head; 38 | } 39 | get(id:number):MessageEvent { 40 | let p = this.head; 41 | while(p) { 42 | if (p.id === id) { 43 | return p; 44 | } 45 | p = p[this.nextName] 46 | } 47 | } 48 | remove(event:MessageEvent) { 49 | const previousEvent = event[this.previousName]; 50 | const nextEvent = event[this.nextName]; 51 | if (previousEvent) { 52 | previousEvent[this.nextName] = nextEvent; 53 | } else { 54 | this.head = nextEvent; 55 | } 56 | if (nextEvent) { 57 | nextEvent[this.previousName] = previousEvent; 58 | } else { 59 | this.tail = previousEvent; 60 | } 61 | this.length--; 62 | } 63 | *[Symbol.iterator]() { 64 | let p = this.head; 65 | while(p) { 66 | yield p; 67 | p = p[this.nextName]; 68 | } 69 | } 70 | dump():string { 71 | let result = `MessageQueue ${this.queueName} [${this.length}] 72 | head: ${this.head.id} 73 | tail: ${this.tail.id} 74 | list: `; 75 | let p = this.head; 76 | while(p) { 77 | result = result + p.id; 78 | p = p[this.nextName]; 79 | if (p) { 80 | result += " > "; 81 | } 82 | } 83 | return result 84 | 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node-RED Flow Debugger 2 | 3 | This module is a Plugin for Node-RED 2.x. It provides a flow debugger with the following 4 | features: 5 | 6 | - set breakpoints on node inputs or outputs 7 | - pause the runtime manually or when a message arrives at a breakpoint 8 | - once paused you can: 9 | - inspect the queued up messages 10 | - step forward individual messages 11 | - drop messages 12 | 13 | ## Installation 14 | 15 | Install this module in your Node-RED user directory or via the Palette Manager 16 | then restart Node-RED 17 | 18 | npm install node-red-debugger 19 | 20 | 21 | ## Usage 22 | 23 | The Flow Debugger adds a new sidebar tab in the editor. Select it from the dropdown 24 | menu. 25 | 26 | By default, the debugger is disabled. Click the 'disabled' toggle button to enable it. 27 | 28 | The sidebar has two sections - a list of the breakpoints you have set and a list 29 | of any messages queued up in the runtime. 30 | 31 | ### Working with breakpoints 32 | 33 | With the debugger enabled, when you hover over a node's port a breakpoint indicator 34 | will appear. Move your mouse over the indicator and click once - it will turn solid blue 35 | and an entry will appear in the sidebar. 36 | 37 | If you click on it again, the breakpoint will be deactivated but remain in place (light blue). 38 | 39 | Clicking on it again will remove the breakpoint entirely (dotted outline). 40 | 41 | You can also deactive a breakpoint using its checkbox in the sidebar, and remove it by 42 | clicking the `x` button. 43 | 44 | ### Pausing the runtime 45 | 46 | The runtime will by paused whenever a message arrives at an active breakpoint. You 47 | can also manually pause the runtime using the pause button in the sidebar. 48 | 49 | Once paused, the flow will show how many messages are queued up at each node input 50 | and output. Those messages will also be listed in the sidebar - in the order the 51 | runtime will process them. 52 | 53 | If you click the step button at the top of the sidebar, the runtime will process 54 | the next message in the list. You can step individual messages by clicking the 55 | step button that appears when you hover over the message. 56 | 57 | You can also delete any message from the queue by clicking its delete button. This 58 | will prevent the message from passing any further in the flow. 59 | 60 | You can click the play button to resume the flows. 61 | 62 | 63 | ## Limitations 64 | 65 | - Due to the way Subflows work, breakpoints on Subflow outputs will be ignored 66 | 67 | ## Roadmap 68 | 69 | - Set conditions on individual breakpoints 70 | - Allow queued messages to be edited 71 | - Pause only selected nodes/flows/groups 72 | 73 | 74 | ## Development 75 | 76 | This plugin has been developed using TypeScript. This means that when running 77 | from the source code rather than npm, it must first be built. 78 | 79 | git clone https://github.com/node-red/node-red-debugger.git 80 | cd node-red-debugger 81 | npm install 82 | npm run build 83 | 84 | This will generate all of the plugin files in the `dist` folder - which is where 85 | Node-RED will expect to load the files from. 86 | 87 | 88 | Then, in your Node-RED user directory (`~/.node-red`) run: 89 | 90 | npm install `` 91 | 92 | ### Themeing 93 | 94 | The Debugger sidebar will use the active Node-RED theme. For the breakpoints 95 | drawn within the flow workspace, the following CSS variables will be used if they 96 | are set by the active theme. 97 | 98 | - `--red-ui-flow-debugger-breakpoint-fill` 99 | - `--red-ui-flow-debugger-breakpoint-stroke` 100 | - `--red-ui-flow-debugger-breakpoint-active-fill` 101 | - `--red-ui-flow-debugger-breakpoint-active-stroke` 102 | - `--red-ui-flow-debugger-breakpoint-inactive-fill` 103 | - `--red-ui-flow-debugger-breakpoint-inactive-stroke` 104 | - `--red-ui-flow-debugger-breakpoint-label` 105 | - `--red-ui-flow-debugger-breakpoint-label-active` 106 | -------------------------------------------------------------------------------- /src/flow-debugger.ts: -------------------------------------------------------------------------------- 1 | import {Debugger} from "./lib/debugger" 2 | import {PausedEvent} from "./lib/types" 3 | import {Location} from "./lib/location" 4 | 5 | module.exports = (RED:any) => { 6 | 7 | const apiRoot = "/flow-debugger"; 8 | 9 | RED.plugins.registerPlugin("node-red-debugger", { 10 | onadd: () => { 11 | 12 | const flowDebugger = new Debugger(RED); 13 | const routeAuthHandler = RED.auth.needsPermission("flow-debugger.write"); 14 | 15 | RED.comms.publish("flow-debugger/connected",true, true) 16 | 17 | function publishState() { 18 | RED.comms.publish("flow-debugger/state",flowDebugger.getState()) 19 | } 20 | 21 | flowDebugger.on("paused", (event:PausedEvent) => { 22 | RED.comms.publish("flow-debugger/paused",event) 23 | }) 24 | 25 | flowDebugger.on("resumed", (event:PausedEvent) => { 26 | RED.comms.publish("flow-debugger/resumed",event) 27 | }) 28 | flowDebugger.on("messageQueued", (event) => { 29 | // Don't include the full message on the event 30 | // event.msg = RED.util.encodeObject({msg:event.msg}, {maxLength: 100}); 31 | delete event.msg; 32 | RED.comms.publish("flow-debugger/messageQueued",event) 33 | }); 34 | flowDebugger.on("messageDispatched", (event) => { 35 | RED.comms.publish("flow-debugger/messageDispatched",event) 36 | }); 37 | // flowDebugger.on("step", (event) => { 38 | // 39 | // }); 40 | 41 | RED.httpAdmin.get(`${apiRoot}`, (_:any, res:any) => { 42 | res.json(flowDebugger.getState()); 43 | }); 44 | 45 | RED.httpAdmin.put(`${apiRoot}`, routeAuthHandler, (req:any, res:any) => { 46 | let stateChanged = false; 47 | if (req.body.hasOwnProperty("enabled")) { 48 | const enabled = !!req.body.enabled; 49 | if (enabled && !flowDebugger.enabled) { 50 | flowDebugger.enable(); 51 | stateChanged = true; 52 | } else if (!enabled && flowDebugger.enabled) { 53 | flowDebugger.disable(); 54 | stateChanged = true; 55 | } 56 | } 57 | if (req.body.hasOwnProperty("config")) { 58 | stateChanged = flowDebugger.setConfig(req.body.config); 59 | } 60 | if (stateChanged) { 61 | publishState(); 62 | } 63 | res.json(flowDebugger.getState()); 64 | }); 65 | 66 | RED.httpAdmin.get(`${apiRoot}/breakpoints`, routeAuthHandler, (_:any, res:any) => { 67 | res.json(flowDebugger.getBreakpoints()); 68 | }) 69 | RED.httpAdmin.put(`${apiRoot}/breakpoints/:id`, routeAuthHandler, (req:any, res:any) => { 70 | flowDebugger.setBreakpointActive(req.params.id, req.body.active) 71 | res.json(flowDebugger.getBreakpoint(req.params.id)); 72 | }) 73 | RED.httpAdmin.delete(`${apiRoot}/breakpoints/:id`, routeAuthHandler, (req:any, res:any) => { 74 | flowDebugger.clearBreakpoint(req.params.id) 75 | res.sendStatus(200) 76 | }) 77 | RED.httpAdmin.post(`${apiRoot}/breakpoints`, routeAuthHandler, (req:any, res:any) => { 78 | // req.body.location 79 | const breakpointId = flowDebugger.setBreakpoint(new Location(req.body.id,req.body.path,req.body.portType,req.body.portIndex)) 80 | res.json(flowDebugger.getBreakpoint(breakpointId)); 81 | }) 82 | RED.httpAdmin.get(`${apiRoot}/messages`, routeAuthHandler, (_:any, res:any) => { 83 | res.json(Array.from(flowDebugger.getMessageQueue()).map(m => { 84 | const result = { 85 | id: m.id, 86 | location: m.location.toString(), 87 | destination: undefined, 88 | msg: RED.util.encodeObject({msg:m.event.msg}, {maxLength: 100}) 89 | } 90 | if (m.event.hasOwnProperty('source')) { 91 | // SendEvent - so include the destination location id 92 | result.destination = m.event.destination.id+"[i][0]" 93 | } 94 | return result; 95 | })) 96 | }); 97 | RED.httpAdmin.get(`${apiRoot}/messages/:id`, routeAuthHandler, (req:any, res:any) => { 98 | const id = req.params.id; 99 | const messageEvent = flowDebugger.getMessageQueue().get(parseInt(id,10)); 100 | if (messageEvent) { 101 | const result = { 102 | id: messageEvent.id, 103 | location: messageEvent.location, 104 | destination: undefined, 105 | msg: RED.util.encodeObject({msg:messageEvent.event.msg}, {maxLength: 100}) 106 | } 107 | if (messageEvent.event.hasOwnProperty('source')) { 108 | // SendEvent - so include the destination location id 109 | result.destination = messageEvent.event.destination.id+"[i][0]" 110 | } 111 | res.json(result) 112 | } else { 113 | res.sendStatus(404); 114 | } 115 | }); 116 | RED.httpAdmin.delete(`${apiRoot}/messages/:id`, routeAuthHandler, (req:any, res:any) => { 117 | flowDebugger.deleteMessage(parseInt(req.params.id,10)); 118 | res.sendStatus(200); 119 | }); 120 | RED.httpAdmin.post(`${apiRoot}/pause`, routeAuthHandler, (_:any, res:any) => { 121 | flowDebugger.pause(); 122 | res.sendStatus(200); 123 | }); 124 | RED.httpAdmin.post(`${apiRoot}/step`, routeAuthHandler, (req:any, res:any) => { 125 | let stepMessage = null; 126 | if (req.body && req.body.message) { 127 | stepMessage = req.body.message; 128 | } 129 | flowDebugger.step(stepMessage); 130 | res.sendStatus(200); 131 | }); 132 | 133 | RED.httpAdmin.post(`${apiRoot}/resume`, routeAuthHandler, (_:any, res:any) => { 134 | flowDebugger.resume(); 135 | res.sendStatus(200); 136 | }); 137 | } 138 | }) 139 | } 140 | /* 141 | 142 | /flow-debugger/enable 143 | /flow-debugger/disable 144 | /flow-debugger/breakpoint 145 | 146 | 147 | 148 | */ 149 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | .red-ui-flow-debugger { 2 | height: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | .red-ui-flow-debugger-stack { 7 | overflow-y: scroll; 8 | background: var(--red-ui-primary-background); 9 | } 10 | .red-ui-flow-debugger-stack .red-ui-palette-category:last-child { 11 | border-bottom: 1px solid var(--red-ui-secondary-border-color); 12 | } 13 | .red-ui-flow-debugger.disabled .red-ui-sidebar-header > :not(:first-child) { 14 | display: none; 15 | } 16 | .red-ui-flow-debugger.disabled .red-ui-flow-debugger-stack { 17 | display: none; 18 | } 19 | .red-ui-flow-debugger .red-ui-sidebar-header { 20 | flex: 0 0 auto; 21 | display: flex; 22 | } 23 | .red-ui-flow-debugger .red-ui-sidebar-header > * { 24 | flex-shrink: 0; 25 | } 26 | .red-ui-flow-debugger .red-ui-editableList-border { 27 | border: none; 28 | border-radius: 0; 29 | } 30 | .red-ui-flow-debugger .red-ui-editableList-container { 31 | padding: 0; 32 | } 33 | .red-ui-flow-debugger .red-ui-editableList-container li { 34 | padding: 0; 35 | } 36 | .red-ui-flow-debugger .red-ui-editableList-container li .red-ui-editableList-item-remove { 37 | right: 3px; 38 | } 39 | .red-ui-flow-debugger-breakpoint-list, .red-ui-flow-debugger-message-list { 40 | min-height: 30px; 41 | } 42 | .red-ui-flow-debugger-breakpoint-list .red-ui-editableList-item-content { 43 | padding: 6px 8px; 44 | } 45 | .red-ui-flow-debugger-message-list .red-ui-editableList-item-content { 46 | padding: 6px 0px; 47 | } 48 | .red-ui-flow-debugger-message .red-ui-debug-msg-meta { 49 | display: flex; 50 | padding-left: 10px; 51 | } 52 | .red-ui-flow-debugger-breakpoint { 53 | user-select: none; 54 | display: flex; 55 | align-items: center; 56 | } 57 | 58 | .red-ui-flow-debugger-breakpoint .red-ui-node-icon { 59 | margin-left: 6px; 60 | margin-right: 6px; 61 | } 62 | 63 | .red-ui-flow-debugger-breakpoint.red-ui-flow-debugger-breakpoint-input .red-ui-node-icon:before, 64 | .red-ui-flow-debugger-breakpoint.red-ui-flow-debugger-breakpoint-output .red-ui-node-icon:after { 65 | position: absolute; 66 | top: calc(50% - 4px); 67 | left: -5px; 68 | width: 7px; 69 | height: 7px; 70 | display: block; 71 | content: ""; 72 | background: var(--red-ui-node-port-background); 73 | border: 1px solid var(--red-ui-node-border); 74 | border-radius: 2px; 75 | } 76 | 77 | .red-ui-flow-debugger-breakpoint.red-ui-flow-debugger-breakpoint-output .red-ui-node-icon:after { 78 | left: 20px; 79 | } 80 | .red-ui-flow-debugger .red-ui-editableList-container li.highlight { 81 | background: #e6e6e6; 82 | } 83 | .red-ui-flow-debugger .red-ui-editableList-container li.pending { 84 | opacity: 0.4; 85 | } 86 | .red-ui-flow-debugger-breakpoint input[type=checkbox] { 87 | margin-top: 0; 88 | margin-right: 4px; 89 | } 90 | .red-ui-flow-debugger-breakpoint > :nth-child(2) { 91 | flex-grow: 1; 92 | flex-shrink: 1; 93 | overflow: hidden; 94 | } 95 | .red-ui-flow-debugger-breakpoint:not(.active) *:not(:first-child) { 96 | opacity: 0.7; 97 | } 98 | .red-ui-flow-debugger-breakpoint-port { 99 | /* padding-left: 5px; 100 | font-size: 0.9em; 101 | width: 55px; */ 102 | flex-shrink: 0; 103 | height: 22px; 104 | } 105 | .red-ui-flow-debugger-breakpoint-port svg { 106 | stroke:#f00; 107 | stroke-width: 1; 108 | fill: none; 109 | } 110 | 111 | /* Flow Annotations */ 112 | .red-ui-flow-debugger-port-overlay { 113 | cursor: pointer; 114 | display: none; 115 | } 116 | 117 | .red-ui-flow-debugger-port-overlay-background { 118 | fill: var(--red-ui-flow-debugger-breakpoint-fill, #fff); 119 | stroke-width: 1; 120 | stroke: var(--red-ui-flow-debugger-breakpoint-stroke, #58d); 121 | } 122 | 123 | .red-ui-flow-debugger-port-overlay-background.red-ui-flow-debugger-port-overlay-pulse { 124 | fill: none !important; 125 | stroke-width: 1; 126 | stroke: var(--red-ui-flow-debugger-breakpoint-stroke, #58d); 127 | animation: red-ui-flow-debugger-port-overlay-pulse-kf 0.6s linear; 128 | } 129 | 130 | @keyframes red-ui-flow-debugger-port-overlay-pulse-kf { 131 | 0%{ 132 | transform: scale(0.8); 133 | opacity: 1; 134 | } 135 | 50%{ 136 | opacity: 0.5; 137 | } 138 | 70%{ 139 | opacity: 0.2; 140 | } 141 | 100%{ 142 | transform: scale(2,1.5); 143 | opacity: 0; 144 | } 145 | } 146 | 147 | .red-ui-flow-debugger-port-overlay.active .red-ui-flow-debugger-port-overlay-background { 148 | fill: var(--red-ui-flow-debugger-breakpoint-active-fill, #59e); 149 | stroke: var(--red-ui-flow-debugger-breakpoint-active-stroke, #36b); 150 | } 151 | .red-ui-flow-debugger-port-overlay.inactive .red-ui-flow-debugger-port-overlay-background { 152 | fill: var(--red-ui-flow-debugger-breakpoint-inactive-fill, #def); 153 | stroke: var(--red-ui-flow-debugger-breakpoint-inactive-stroke, #59e); 154 | } 155 | .red-ui-flow-debugger-port-overlay.pending .red-ui-flow-debugger-port-overlay-background { 156 | fill: var(--red-ui-flow-debugger-breakpoint-inactive-fill, #def); 157 | stroke: var(--red-ui-flow-debugger-breakpoint-inactive-stroke, #59e); 158 | stroke-dasharray: 4 2; 159 | opacity: 0.8; 160 | } 161 | .red-ui-flow-debugger-port-overlay-highlight { 162 | stroke-width: 1.5; 163 | fill: none; 164 | stroke-dasharray: 4 2; 165 | stroke: var(--red-ui-node-selected-color, #ff7f0e); 166 | transform: scale(1.2, 1.5); 167 | 168 | } 169 | 170 | .red-ui-flow-debugger-port-overlay.pending, 171 | .red-ui-flow-debugger-port-overlay.active, 172 | .red-ui-flow-debugger-port-overlay.inactive, 173 | .red-ui-flow-debugger-port-overlay.hasMessages { 174 | display: inherit; 175 | } 176 | .red-ui-flow-debugger-port-overlay-label { 177 | stroke-width: 0; 178 | fill: var(--red-ui-flow-debugger-breakpoint-label, var(--red-ui-node-label-color, #000)); 179 | font-size: 9pt; 180 | text-anchor:end; 181 | } 182 | .red-ui-flow-debugger-port-overlay-label.small { 183 | font-size: 7pt; 184 | transform: translate(2px,-1px); 185 | } 186 | .red-ui-flow-debugger-port-overlay.active .red-ui-flow-debugger-port-overlay-label { 187 | 188 | fill: var(--red-ui-flow-debugger-breakpoint-label-active, #fff); 189 | } 190 | 191 | .red-ui-flow-debugger-notification { 192 | display: flex; 193 | } 194 | .red-ui-flow-debugger-notification > .red-ui-flow-debugger-toolbar { 195 | flex-grow: 1; 196 | text-align: right; 197 | } 198 | .red-ui-flow-debugger-message:hover .red-ui-flow-debugger-msg-tools { 199 | display: inline; 200 | } 201 | .red-ui-flow-debugger-msg-tools { 202 | position: absolute; 203 | right: 5px; 204 | display: none; 205 | } 206 | .red-ui-flow-debugger-settings { 207 | background: var(--red-ui-tertiary-background); 208 | color: var(--red-ui-primary-text-color); 209 | } 210 | .red-ui-flow-debugger-settings div { 211 | margin: 0 0 6px 0; 212 | } 213 | .red-ui-flow-debugger-settings label { 214 | display: flex; 215 | align-items: center; 216 | margin-left: 10px; 217 | } 218 | .red-ui-flow-debugger-settings input[type="radio"] { 219 | margin: 0 5px 0 0; 220 | } 221 | .red-ui-flow-debugger-message-load { 222 | display: block; 223 | padding: 0; 224 | font-size: 0.8em; 225 | color: var(--red-ui-tertiary-text-color); 226 | } 227 | .red-ui-flow-debugger-message-load a { 228 | display: block; 229 | padding: 6px 0 8px 25px; 230 | } 231 | .red-ui-flow-debugger-annotation-paused { 232 | stroke: var(--red-ui-flow-debugger-breakpoint-active-stroke, #36b); 233 | 234 | } 235 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright OpenJS Foundation and other contributors, https://openjsf.org/ 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | -------------------------------------------------------------------------------- /src/lib/debugger.ts: -------------------------------------------------------------------------------- 1 | import * as Location from "./location" 2 | import { Breakpoint, PausedEvent, MessageEvent } from "./types" 3 | import { ReceiveEvent, SendEvent, EventCallback } from "../nr-types" 4 | import { MessageQueue } from "./MessageQueue" 5 | import { EventEmitter } from "events" 6 | 7 | const DEBUGGER_PAUSED = Symbol("node-red-debugger: paused"); 8 | 9 | type DebuggerConfig = { 10 | breakpointAction: "pause-all" | "pause-bp" 11 | } 12 | interface MessageQueueTable { 13 | [Key: string]: MessageQueue 14 | } 15 | 16 | let BREAKPOINT_ID = 1; 17 | 18 | export class Debugger extends EventEmitter { 19 | 20 | RED: any; 21 | enabled: boolean; 22 | pausedLocations: Set; 23 | breakpoints: Map; 24 | breakpointsByLocation: Map; 25 | eventNumber: number; 26 | queuesByLocation: MessageQueueTable; 27 | messageQueue: MessageQueue; 28 | config: DebuggerConfig; 29 | 30 | // Events: 31 | // paused / resumed 32 | 33 | constructor(RED: any) { 34 | super(); 35 | this.config = { 36 | breakpointAction: "pause-all" 37 | }; 38 | this.RED = RED; 39 | this.enabled = false; 40 | this.breakpoints = new Map(); 41 | this.pausedLocations = new Set(); 42 | this.breakpointsByLocation = new Map(); 43 | this.queuesByLocation = {}; 44 | this.messageQueue = new MessageQueue("Time"); 45 | this.eventNumber = 0; 46 | } 47 | log(message:string) { 48 | this.RED.log.info(`[flow-debugger] ${message}`) 49 | } 50 | 51 | private checkLocation(location:Location.Location, event:SendEvent|ReceiveEvent, done:EventCallback) { 52 | const breakpointId:string = location.getBreakpointLocation(); 53 | if (this.isNodePaused(location.id)) { 54 | this.queueEvent(location,event,done); 55 | } else { 56 | if (event.msg && event.msg[DEBUGGER_PAUSED]) { 57 | this.pause({ 58 | reason: "step", 59 | node: location.id 60 | }) 61 | this.queueEvent(location,event,done); 62 | } else { 63 | const bp = this.breakpointsByLocation.get(breakpointId); 64 | if (bp && bp.active) { 65 | this.pause({ 66 | reason: "breakpoint", 67 | node: location.id, 68 | breakpoint: bp.id 69 | }) 70 | this.queueEvent(location,event,done); 71 | } else { 72 | done(); 73 | } 74 | } 75 | } 76 | } 77 | 78 | enable() { 79 | this.log("Enabled"); 80 | this.enabled = true; 81 | this.RED.hooks.add("preRoute.flow-debugger", (sendEvent:SendEvent, done:EventCallback) => { 82 | if (isNodeInSubflowModule(sendEvent.source.node)) { 83 | // Inside a subflow module - don't pause the event 84 | done(); 85 | return; 86 | } 87 | if (sendEvent.source.node._flow.TYPE !== "flow" && sendEvent.source.node.id === sendEvent.source.node._flow.id) { 88 | // This is the subflow output which, in the current implementation 89 | // means the message is actually about to be routed to the first node 90 | // inside the subflow, not the output of actual subflow. 91 | done(); 92 | return; 93 | } 94 | 95 | if (sendEvent.cloneMessage) { 96 | sendEvent.msg = this.RED.util.cloneMessage(sendEvent.msg); 97 | sendEvent.cloneMessage = false; 98 | } 99 | const eventLocation = Location.createLocation(sendEvent); 100 | // console.log("preRoute",eventLocation.toString()); 101 | this.checkLocation(eventLocation, sendEvent, done); 102 | }); 103 | this.RED.hooks.add("onReceive.flow-debugger", (receiveEvent:ReceiveEvent, done:EventCallback) => { 104 | if (receiveEvent.destination.node.type === "inject") { 105 | // Never pause an Inject node's internal receive event 106 | done(); 107 | return; 108 | } 109 | if (isNodeInSubflowModule(receiveEvent.destination.node)) { 110 | // Inside a subflow module - don't pause the event 111 | done(); 112 | return; 113 | } 114 | const eventLocation = Location.createLocation(receiveEvent); 115 | // console.log("onReceive",eventLocation.toString()); 116 | this.checkLocation(eventLocation, receiveEvent, done); 117 | }); 118 | } 119 | 120 | disable() { 121 | this.log("Disabled"); 122 | this.enabled = false; 123 | this.RED.hooks.remove("*.flow-debugger"); 124 | this.pausedLocations.clear(); 125 | this.drainQueues(true); 126 | } 127 | pause(event?:PausedEvent) { 128 | if (this.enabled) { 129 | let logReason:string; 130 | if (event) { 131 | if (this.config.breakpointAction === "pause-all") { 132 | this.pausedLocations.clear(); 133 | this.pausedLocations.add("*"); 134 | } else { 135 | this.pausedLocations.add(event.node); 136 | } 137 | if (event.reason === "breakpoint") { 138 | logReason = "@"+this.breakpoints.get(event.breakpoint).location.toString() 139 | } else if (event.reason === "step") { 140 | logReason = "@"+event.node 141 | } 142 | event.pausedLocations = [...this.pausedLocations]; 143 | } else { 144 | // Manual pause 145 | this.pausedLocations.clear(); 146 | this.pausedLocations.add("*"); 147 | logReason = "manual"; 148 | } 149 | this.log(`Flows paused: ${logReason}`); 150 | this.emit("paused", event || { reason: "manual" }) 151 | } 152 | } 153 | resume(nodeId?:string) { 154 | if (this.pausedLocations.size === 0) { 155 | return; 156 | } 157 | if (!nodeId || nodeId === "*") { 158 | console.log("resume - clear all locations") 159 | this.pausedLocations.clear(); 160 | } else if (nodeId && this.pausedLocations.has(nodeId)) { 161 | this.pausedLocations.delete(nodeId); 162 | } else { 163 | // Nothing has been unpaused 164 | return; 165 | } 166 | this.log("Flows resumed"); 167 | this.emit("resumed", { node: nodeId }) 168 | this.drainQueues(); 169 | } 170 | deleteMessage(messageId:number) { 171 | const nextEvent = this.messageQueue.get(messageId); 172 | if (nextEvent) { 173 | this.messageQueue.remove(nextEvent); 174 | const nextEventLocation = nextEvent.location.toString(); 175 | this.queuesByLocation[nextEventLocation].remove(nextEvent); 176 | const queueDepth = this.queuesByLocation[nextEventLocation].length; 177 | if (queueDepth === 0) { 178 | delete this.queuesByLocation[nextEventLocation] 179 | } 180 | this.emit("messageDispatched", { id: nextEvent.id, location: nextEventLocation, depth: queueDepth }) 181 | // Call done with false to prevent any further processing 182 | nextEvent.done(false); 183 | } 184 | } 185 | private isNodePaused(nodeId:string) { 186 | return this.pausedLocations.has("*") || this.pausedLocations.has(nodeId); 187 | } 188 | private drainQueues(quiet?:boolean) { 189 | for (const nextEvent of this.messageQueue) { 190 | const eventNodeId = nextEvent.location.id; 191 | if (!this.isNodePaused(eventNodeId)) { 192 | const nextEventLocation = nextEvent.location.toString(); 193 | this.queuesByLocation[nextEventLocation].remove(nextEvent); 194 | const queueDepth = this.queuesByLocation[nextEventLocation].length; 195 | if (queueDepth === 0) { 196 | delete this.queuesByLocation[nextEventLocation] 197 | } 198 | if (!quiet) { 199 | this.emit("messageDispatched", { id: nextEvent.id, location: nextEventLocation, depth: queueDepth }) 200 | } 201 | if (nextEvent.event.msg[DEBUGGER_PAUSED]) { 202 | delete nextEvent.event.msg[DEBUGGER_PAUSED]; 203 | } 204 | nextEvent.done(); 205 | this.messageQueue.remove(nextEvent); 206 | } 207 | } 208 | } 209 | setBreakpoint(location:Location.Location): string { 210 | const bp:Breakpoint = { 211 | id: (BREAKPOINT_ID++)+"", 212 | location, 213 | active: true, 214 | mode: "all" 215 | } 216 | this.breakpoints.set(bp.id, bp); 217 | this.breakpointsByLocation.set(location.toString(), bp); 218 | return bp.id; 219 | } 220 | getBreakpoint(breakpointId: string) { 221 | return this.breakpoints.get(breakpointId); 222 | } 223 | setBreakpointActive(breakpointId: string, state: boolean) { 224 | const bp = this.breakpoints.get(breakpointId); 225 | if (bp) { 226 | bp.active = state 227 | } 228 | } 229 | 230 | clearBreakpoint(breakpointId: string) { 231 | const bp = this.breakpoints.get(breakpointId); 232 | if (bp) { 233 | this.breakpoints.delete(breakpointId); 234 | this.breakpointsByLocation.delete(bp.location.toString()); 235 | } 236 | } 237 | 238 | getBreakpoints(): Breakpoint[] { 239 | return Array.from(this.breakpoints.values()); 240 | } 241 | 242 | step(messageId?:number) { 243 | if (this.enabled) { 244 | let nextEvent:MessageEvent; 245 | if (messageId) { 246 | nextEvent = this.messageQueue.get(messageId); 247 | if (nextEvent) { 248 | this.messageQueue.remove(nextEvent); 249 | } 250 | } else { 251 | nextEvent = this.messageQueue.next(); 252 | } 253 | if (nextEvent) { 254 | const nextEventLocation = nextEvent.location.toString(); 255 | this.log("Step: "+nextEventLocation); 256 | 257 | this.queuesByLocation[nextEventLocation].remove(nextEvent); 258 | const queueDepth = this.queuesByLocation[nextEventLocation].length; 259 | if (queueDepth === 0) { 260 | delete this.queuesByLocation[nextEventLocation] 261 | } 262 | nextEvent.event.msg[DEBUGGER_PAUSED] = true; 263 | this.emit("messageDispatched", { id: nextEvent.id, location: nextEventLocation, depth: queueDepth }) 264 | nextEvent.done(); 265 | } 266 | } 267 | } 268 | 269 | setConfig(newConfig: object): boolean { 270 | let changed = false; 271 | for (const key in this.config) { 272 | if (newConfig.hasOwnProperty(key) && this.config[key] !== newConfig[key]) { 273 | changed = true; 274 | this.config[key] = newConfig[key]; 275 | } 276 | } 277 | return changed; 278 | } 279 | 280 | getState(): object { 281 | if (!this.enabled) { 282 | return { enabled: false } 283 | } 284 | return { 285 | enabled: true, 286 | pausedLocations: [...this.pausedLocations], 287 | config: this.config, 288 | breakpoints: this.getBreakpoints(), 289 | queues: this.getMessageQueueDepths() 290 | } 291 | } 292 | getMessageSummary() { 293 | return Array.from(this.messageQueue).map(m => { 294 | return { 295 | id: m.id, 296 | location: m.location 297 | } 298 | }) 299 | } 300 | getMessageQueue(): MessageQueue { 301 | return this.messageQueue; 302 | } 303 | 304 | getMessageQueueDepths(): object { 305 | if (!this.enabled) { 306 | return {}; 307 | } 308 | const result = {}; 309 | for (const [locationId, queue] of Object.entries(this.queuesByLocation)) { 310 | result[locationId] = { depth: queue.length } 311 | } 312 | return result; 313 | } 314 | 315 | 316 | dump():string { 317 | let result = `Debugger State 318 | --- 319 | ${this.messageQueue.dump()} 320 | `; 321 | const locationIds = Object.keys(this.queuesByLocation); 322 | locationIds.forEach(id => { 323 | result += `--- 324 | Location: ${id} 325 | ${this.queuesByLocation[id].dump()} 326 | `; 327 | }) 328 | return result; 329 | } 330 | private queueEvent(location:Location.Location, event:SendEvent|ReceiveEvent, done:EventCallback) { 331 | const locationId = location.toString(); 332 | if (!this.queuesByLocation[locationId]) { 333 | this.queuesByLocation[locationId] = new MessageQueue("Location"); 334 | } 335 | const messageEvent:MessageEvent = { 336 | id: this.eventNumber++, 337 | event, 338 | location, 339 | done, 340 | nextByLocation: null, 341 | previousByLocation: null, 342 | nextByTime: null, 343 | previousByTime: null 344 | } 345 | this.queuesByLocation[locationId].enqueue(messageEvent); 346 | this.messageQueue.enqueue(messageEvent); 347 | const queuedEvent = { 348 | id: messageEvent.id, 349 | location: locationId, 350 | msg: event.msg, 351 | depth: this.queuesByLocation[locationId].length, 352 | destination: null, 353 | }; 354 | if (event.hasOwnProperty('source')) { 355 | // SendEvent - so include the destination location id 356 | queuedEvent.destination = "/"+event.destination.id+"[i][0]" 357 | } 358 | this.emit("messageQueued", queuedEvent) 359 | } 360 | } 361 | 362 | const MODULE_TYPE_RE = /^module:/; 363 | 364 | function isNodeInSubflowModule(node:any) { 365 | let f = node._flow; 366 | do { 367 | if (f.TYPE === "flow") { 368 | return false; 369 | } 370 | if (MODULE_TYPE_RE.test(f.TYPE)) { 371 | return true; 372 | } 373 | f = f.parent; 374 | } while(f && f.TYPE); 375 | return false; 376 | } 377 | -------------------------------------------------------------------------------- /src/flow-debugger.html: -------------------------------------------------------------------------------- 1 | 2 | 1046 | --------------------------------------------------------------------------------