
I somehow found some free time to pick up my OPL language server project again a couple of days ago and realised it had a glaring bug that would have caused mayhem for anyone who might have attempted to use it since I first published the code earlier this year. Placing the language server logic inside a while 1=1 loop was, it turns out, not such a smart idea, because in so doing, the CPU simply ran the language server code again and again and again forever, gradually creeping up to using 100% of available CPU resource. Considering my language server doesn't do an awful lot (yet) besides supporting a handful of auto-completes, this felt sub-optimal.
My initial assumption was that I needed to change the basis of the loop, using the existing input pipe stream's NumBytesAvailable property. However, this resulted in somewhat chaotic results, including mangled request messages from the IDE and subsequent failure by my language server to handle them. The reason for this, I believe, is because I was trying to start my loop before the handshake between IDE and language server had completed, and so wasn't giving the program a chance to get going. I therefore pushed all of the code that manages the ‘check for request → provide response’ into a procedure, called this procedure and only then went on to form the loop (which also calls that same procedure, but only after the handshake has happened).
The loop was still problematic, however, and runaway CPU issues persisted. I experimented with a stream reader to see if this would make a difference, but unfortunately Pascal's TStreamReader class only supports the ReadLine() method, which I had already concluded would not work here because of the CRLF control characters that form part of every request message under the LSP. That said, it helped remind me that EOF could still be used as a basis for the loop, and that I could combine this with a conditional check on NumBytesAvailable to prevent the loop doing anything unless it had to. The final remedy was to add in a sleep(1) call in the else part of this conditional, which magically stops the program hogging all available CPU resource. Putting this all together, I solved the runaway CPU bug and was all set to move on to adding new request handlers…
…which was just as well, because I realised at this point that I had no handler for the method="shutdown" request! The net result of that is that despite quitting the IDE, my language server program carried on running in the background, and each new startup of the language server from a fresh IDE session did the same. Accumulating lots of these zombie processes was clearly not desirable, but as the IDE fires off an “I'm shutting down now” message when it exits, it was easy enough to add an additional handler that, on encountering this request message, was then able to issue a halt(0) to kill the language server program. Interestingly, Pascal's exit function only exits the current subroutine, but if that subroutine is not the main program (as was the case for me) it won't stop the program, it merely hands control over to whatever called the subroutine. Handily, halt(0) will stop the program regardless of where it is called from.
All code changes are now committed and pushed to the main branch of my repo, so you can safely experiment with this (albeit slightly bare) language server for yourself, without having to worry that it's going to hoover up all of your CPU resource and eventually crash your machine! I is smart.