// runs the language server inside the extension, in the tinyvm javascript vm, // so nothing has to be built or installed for the server to start. it presents // the same surface the client uses on a child process: stdin to write, stdout // and stderr to listen to, an exit event and kill(). // // the vm is streaming: the server runs until it wants input, the client's bytes // are fed in and the server goes on from there. each step is synchronous, so a // long check holds the extension host for as long as it takes. // // opts.log(level, text) gets what the vm does: the image it loads, each halt, // and the messages the server writes to its error stream. const path = require('path'); const { EventEmitter } = require('events'); function vmProcess(dir, opts = {}) { const log = opts.log || (() => {}); const { TinyVM } = require(path.join(dir, 'tinyvm.js')); const image = require(path.join(dir, 'lsp.json')); const words = require('fs').readFileSync(path.join(dir, 'lsp.bin')); const proc = new EventEmitter(); const out = new EventEmitter(); const err = new EventEmitter(); const pending = []; let dead = false; let bytesIn = 0, bytesOut = 0, steps = 0; const vm = new TinyVM({ streaming: true, memoryWords: image.layout.HEAP_HI, // the initial heap; the machine grows on the heap's request layout: image.layout, onOutput: b => { pending.push(b); }, onMessage: m => { log('problem', 'server says: ' + String(m).slice(0, 500)); err.emit('data', Buffer.from(m, 'utf8')); }, }); const loaded = vm.loadImage(words, image.gorg); log('message', 'loaded the image: ' + loaded + ' words, code at ' + image.gorg); const flush = () => { if (pending.length) { const chunk = Buffer.from(pending); bytesOut += chunk.length; pending.length = 0; out.emit('data', chunk); } }; const after = status => { steps++; flush(); if (status !== 3 && !dead) { // it halted: the server has exited dead = true; log(status === 0 ? 'message' : 'problem', 'the vm halted with status ' + status + ' after ' + steps + ' steps, ' + bytesIn + ' bytes in, ' + bytesOut + ' out'); proc.emit('exit', status); } else if (status === 3 && vm.polling) { // the server asked whether input is waiting: let the event loop deliver it, then go on setImmediate(() => { if (!dead && vm.polling) after(vm.resume()); }); } }; proc.stdin = { writable: true, write(buf) { if (dead) { log('problem', 'write to a halted vm dropped (' + buf.length + ' bytes)'); return false; } bytesIn += buf.length; vm.feed(buf); after(vm.resume()); return true; }, }; proc.stdout = out; proc.stderr = err; proc.kill = () => { if (dead) return; dead = true; log('message', 'vm killed after ' + steps + ' steps'); proc.emit('exit', null); }; const t0 = Date.now(); after(vm.run(image.gorg)); log('message', 'vm ran to its first wait in ' + (Date.now() - t0) + ' ms'); return proc; } module.exports = { vmProcess };