Repository navigation
proposal: --inspect-store flag #298
Description
Hi,
I'd like to propose a solution for following problem: how to discover inspector web socket URL for locally running node instances. Imagine we have a tool that inspects all running node instances and capture some information using inspector protocol. It might be process and its child processes or set of independent node processes.
When we have more then one process to inspect they can not use the same port, so there is no way to define this port ahead of time So we need to pass 0 as port to allow node find free port for inspector by itself. We can pass it using environment and then all processes started with this flag will get own port.
In case of child process we still can parse stderr and try to find magic line that contains full WebSocket URL. It works only when we control process that we would like to start. In a lot of cases it is not convenient or not possible, e.g. we'd like to debug npm run test command as is without passing it to some magic runner script.
So I would like to propose new node flag --inspect-store which takes a folder as argument. When this flag is passed directly or using NODE_OPTIONS, every time when inspector server started in node it will put special random-id file to passed folder. File will contain dump of /json/list endpoint (it contains some information about node including process title and inspector web socket URL). When inspector server finished node will try to remove this file without strict contract - tools that use this flag should be ready for stale files inside and should cleanup this folder after it is finished.
I prefer this flag to be implemented in native node code to avoid any pollution of node runtime with some user land code, e.g. we can use inspector module to actually start inspector, get its url and save to file but it will force loading of inspector and fs modules that can change program behavior with and without inspection.
As alternative we might consider implementing some kind of inspector port that can be predicted but in this case tool will need to constantly ping different ports and waste CPU time.
Another alternative: we can add flag that forces node to send inspector web socket URL back using some channel, e.g. by sending get request to given URL. It requires daemon that should always be available in background. This solution sound more complicated to me than --inspector-store flag.
PoC: nodejs/node#27605
Activity
addaleax commentedon May 8, 2019
So … my first intuitive preference would be to “let userland handle this” – it’s not too complicated to set
NODE_OPTIONS="--require /path/to/script.js", where the script readsinspector.url()and maybe some process metadata and stores that where it’s best fit… the difference to--inspector-storebeing that it’s much more flexible, but doesn’t come out-of-the-box?eugeneo commentedon May 8, 2019
I feel bit queasy about accessing the filesystem on Node.js startup - so much can go wrong. No access to a writable FS might also be a common problem...
Command line flag also requires controlling the process.
Is anything other then URL (or even port) required? Given the port one can always access the /json/list endpoint for that process.
Would it be possible to solve this by having "parent" instance in fork and cluster scenarios cover this role? Then parent instances could publish child instances in their
/json/listand/or pass them up the hierarchy. I am assuming inspector URL discovery for the "root" instance is a solved problem.One problem I see is if the "hierarchy chain" is broken on some level because some process terminated.
alexkozy commentedon May 8, 2019
I have experience using this approach inside ndb and it comes with following disadvantages:
alexkozy commentedon May 8, 2019
I am writing tool so I should create something writable before passing it to node. If node can not write to file then it won't and tool won't detect processes and should fix it on its side. We already have some flags that dumps information to file e.g.
--cpu-prof-name.NODE_OPTIONS=--inspect-store=/tmp/store node script.jsAs well as you can define this environment variable using any other way.
Only url with port are required but I believe that if we are dumping something - it is nice to dump something already available to not create new format for almost the same information.
In general if we run anything - it means that we run something differently in comparison with normal run and most implementations should be as simple as possible to avoid pollution of inspecting process so they should get url as fast as possible and dump it using as less code as possible. As soon as you get more complicated logic there user will be closer and closer to a point when they inspect/debug this code than actual node scripts.
It can be worse, inspected processes might not know about each other, imagine that there are multiple processes running on cron and you'd like to build a tool for them. With my solution you create folder, setup
NODE_OPTIONS=--inspect-store=/tmp/storein environment and it works. Any central orchestration is not needed.hybrist commentedon May 8, 2019
Not sure I follow this part. Wouldn't writing to a file, picking up the change, and then trying to connect also realistically miss the startup of node?
alexkozy commentedon May 8, 2019
If in addition to
--inspect-storeyou pass--inspect-brkit will block in native code waiting until someone connected and sendRuntime.runIfWaitingForDebugger. So tool have time to send any other preparation command before run, e.g.Profiler.startPreciseCoverage. Node won't stop at breakpoint in this case since tool won't passDebugger.enableto enable debugger if it does not need it.So writing files and waiting for
Runtime.runIfWaitingForDebuggerhappens in native code before any node boostrap or user land code.joyeecheung commentedon May 9, 2019
The file specified by
--cpu-prof-nameis written on process exit. There are plenty of other options that specify files to be accessed during startup, though, like--experimental-policy. If environment variables count (since you'd also need control of the process) then there are files accessed even earlier likeNODE_REPL_HISTORYorNODE_EXTRA_CA_CERTS. (I actually think the feature proposed here is more akin toNODE_REPL_HISTORY?)What is the use case of this flag? Is the consumer looking for the endpoint given a PID, or is it trying to find all Node.js process with such endpoints open and these endpoints?
alexkozy commentedon May 9, 2019
It looks like we use a lot of files in Node, I definitely can follow all best practice in this area. Thanks for these examples.
Consumer is trying to find all Node.js processes started inside current process environment with such endpoints. Process environment has nice property that it is inherited by default by child processes.
There are two use cases:
export NODE_OPTIONS='--inspect-store=/tmp/store --inspect-brk=0'there and later any process started from that terminal session will be automatically detected byndb. E.g. I have ndb with pause on exception checkbox enabled in background, startnpm run test, ndb automatically detects npm process and any child test process, pause on exception and bring itself to front.eugeneo commentedon May 9, 2019
Can there be just a single file that gets appended?
alexkozy commentedon May 9, 2019
In theory yes but in this case different node processes will need to implement some kind of synchronization between them to not messed up each other additions.
At the same time I believe that inspector should remove this file at the end when it is possible. Removing lines from the middle of the file definitely requires synchronization.
If node has native support for some file based database - it can be used here, but I believe that all file based databases are implemented in user land.
[-]proposal: --inspector-store flag[/-][+]proposal: --inspect-store flag[/+]joyeecheung commentedon May 10, 2019
Thanks for the explanation.
As I understand, the consumer does not need to discover and attach to child processes after they are already started? I wonder whether we could do the reverse: instead of having the consumer reading something from a folder, we actively notify the consumer from the Node.js instances - e.g. through a pipe or a domain socket, i.e. something like our
PipeWrap- when the inspector server starts listening. It may be less prone to race conditions that way. IIUC, that's also how we notify child processes from the master that the inspector is started in the cluster implementation.alexkozy commentedon May 10, 2019
I do not want to limit consumer in this way. If consumer would like to have, scheduled using cron, script that starts from time to time, detect all running node instances and captures sampling heap profile using protocol to aggregate this data later - it sounds like pretty valid use case. With proposed solution consumer needs to pass
NODE_OPTIONS='--inspect-store=/tmp/store --inspect'in environment to get this behavior.Reverse connection might give us a lot of more flexibility over how information about discovered node processes is stored. I like it but at the same time it adds requirement for some demon that is running in background and requires some cpu cycles. I actually tried reverse connection with
ndbbut it feels more complicated to me and less reliable.I am not sure about what race we are talking about, could you describe potential race in more details? With
--inspect-brknode will wait connection andRuntime.runIfWaitingForDebuggercommand over protocol, so consumer has control over when node instance will start and do any preparation work.alexkozy commentedon May 11, 2019
I was thinking about current available capabilities a little bit more and I can say that parsing
stderrto get WebSocket to debug child processes has one crucial issue. If you pass--inspect-brkusingNODE_OPTIONSand parent process ignores its child's stderr stream then child will wait for connection forever since there is no any way to detect child's websocket port.--inspect-storesolves this problem.joyeecheung commentedon May 11, 2019
I see, that's fair.
What happens if
process.debugPortis mutated? There are alsoprocess._debugProcess(),process._debugEnd()(unfortunately not deprecated), andSIGUSR1, I imagine coordination with these may be prone to races. Though these should be edge cases that come from how the user uses the inspector so they are the one to fix it.I think there are some trade-offs here. Using the file system means we are adding another layer of states to manage, which may be out of sync with the actual states in the processes (since we are not managing those in-memory). For instance, what if the process crashes without having a chance to clean up the file?
7 remaining items
alexkozy commentedon Jun 14, 2019
Since this problem still exists and ndb (and any other tool that needs to debug child processes) suffers without proposed flag.
I would like to propose alternative solution. In addition to files Node.js implements IPC socket and server for IPC communications so my new pull request implements
--inspect-publish-uid-ipc-path, when this flag is there, inspector will try to send web socket url to given named socket.Any thoughts?
hediet commentedon Jun 20, 2019
This is exactly what easy-attach is doing. It does not use
inspector.url()though, but rather connects to the debugger itself using websockets. It then sends this url to VS Code, again using websockets. It works pretty well.alexkozy commentedon Jun 20, 2019
easy-attachhelps with a different use case when user should inject some JS in the target process. I believe that you can useinspectormodule to make code a little bit cleaner and avoid using undocumentedprocess._debugProcess.This issue is about discovering inspector port without any change in user code.
hediet commentedon Jun 20, 2019
@ak239 You can still inject
easy-attachwithNODE_OPTIONS="--require ..."without any change in user code.alexkozy commentedon Jun 20, 2019
@hediet
ndbuses this approach nowadays - it injects preload.js. It is definitely fine for debugging user code but when we need it for cpu or memory profiling - I'd like to get clear picture without any injected code. At the same time with this approach it is not possible to debug/profile bootstrap code.At the same time I found injecting module from global package using
--requirea little bit painful, e.g. it was not possible to use spaces in a path on windows at least in Node 8.If we already publish
inspector.url()tostderrand using/json/listendpoint, I believe we can expose it using another channel as well. Channel that is friendly for tools that do not want to think a lot about how and what code should be injected.In general we can workaround a lot of things, the question here how friendly platform should be for tools.
alexkozy commentedon Jun 20, 2019
@hediet I thought about your solution a little bit more and I think that there is no proper way to emulate sync waiting for debugger to connect except
inspector.openwithtrueas last flag. The problem here that any debugger or tool internally do some variation of:Debugger.enable.Runtime.runIfWaitingForDebuggerto start node.This contract is crucial to avoid possible race conditions. Node is not processing any inspector related activity when it is blocked on any sync call. I believe that you added
waitSomeCyclesfunction to address this problem.inspector.openruns nested message loop, waits for connection, process all protocol commands and only whenRuntime.runIfWaitingForDebuggeris called - it allows node to continue.hediet commentedon Jun 21, 2019
@ak239 That sounds nice, thanks man :) I will definitely explore the
inspectormodule and see what I can do with it.waitSomeCyclesis only one part to wait for the debugger - the other part is the debugger proxy that is launched and inspects the messages sent to the debugger and waits for certain events.alexkozy commentedon Sep 12, 2019
With the recent
inspector.waitForDebuggeraddition, userland solution works fine to me. I will close this one for now.hediet commentedon Sep 24, 2019
@ak239 thanks, I use
inspector.opennow and it works great! I also usechrome-remote-interfaceto step out and continue if no one wants to attach. Its quite funny to have a program that launches another that debugs the first :D