1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
|
// runs the language server as WebAssembly (server/lsp.wasm, built by
// src/tools/mklspwasm.sh): the microcore with the server's image inside. it presents
// the same surface as vmProcess: stdin to write, stdout and stderr to listen to,
// an exit event and kill(). it is much faster than the javascript vm, because
// the server's 64-bit words are native here and not bigints.
//
// the module is synchronous to load and to run, so the server starts in the
// extension host's own thread. a step runs until the server waits for input,
// then returns, the way vmProcess does.
//
// opts.log(level, text) gets what happens: the module loaded, each halt, and the
// messages the server writes to its error stream.
const fs = require('fs');
const { EventEmitter } = require('events');
const CHUNK = 65536; // the most a single feed can carry (wasm_chunk_cap)
function wasmProcess(file, opts = {}) {
const log = opts.log || (() => {});
const t0 = Date.now();
const bytes = fs.readFileSync(file);
const mod = new WebAssembly.Module(bytes);
log('message', 'compiled ' + file + ' (' + bytes.length + ' bytes) in ' + (Date.now() - t0) + ' ms');
const proc = new EventEmitter();
const out = new EventEmitter();
const err = new EventEmitter();
let outBytes = [], msgBytes = [];
let dead = false, steps = 0, bytesIn = 0, bytesOut = 0;
const inst = new WebAssembly.Instance(mod, {
env: {
js_putc: c => { outBytes.push(c & 255); },
js_msg: c => { msgBytes.push(c & 255); },
},
});
const x = inst.exports;
if (x.wasm_chunk_cap() < CHUNK) throw new Error('lsp.wasm has a smaller input chunk than this host expects');
const flush = () => {
if (outBytes.length) {
const chunk = Buffer.from(outBytes);
outBytes = [];
bytesOut += chunk.length;
out.emit('data', chunk);
}
if (msgBytes.length) {
const text = Buffer.from(msgBytes).toString('utf8');
msgBytes = [];
log('problem', 'server says: ' + text.trim().slice(0, 500));
err.emit('data', Buffer.from(text, 'utf8'));
}
};
const after = status => {
steps++;
flush();
if (status !== 3 && !dead) { // it halted: the server has exited
dead = true;
log(status === 0 ? 'message' : 'problem', 'the server halted with status ' + status + ' after ' + steps + ' steps, ' + bytesIn + ' bytes in, ' + bytesOut + ' out');
proc.emit('exit', status);
} else if (status === 3 && x.wasm_polling && x.wasm_polling()) {
// the server asked whether input is waiting: let the event loop deliver it, then go on
setImmediate(() => { if (!dead && x.wasm_polling && x.wasm_polling()) after(x.wasm_resume()); });
}
};
proc.stdin = {
writable: true,
write(buf) {
if (dead) { log('problem', 'write to a halted server dropped (' + buf.length + ' bytes)'); return false; }
bytesIn += buf.length;
const chunkAt = x.wasm_chunk();
let off = 0;
while (off < buf.length) {
const n = Math.min(CHUNK, buf.length - off);
new Uint8Array(x.memory.buffer).set(buf.subarray(off, off + n), chunkAt);
if (x.wasm_feed(n) !== 0) { log('problem', 'the server input is full; dropped ' + (buf.length - off) + ' bytes'); return false; }
off += n;
}
after(x.wasm_resume());
return true;
},
};
proc.stdout = out;
proc.stderr = err;
proc.kill = () => {
if (dead) return;
dead = true;
log('message', 'server killed after ' + steps + ' steps');
proc.emit('exit', null);
};
const t1 = Date.now();
after(x.wasm_start());
log('message', 'server ran to its first wait in ' + (Date.now() - t1) + ' ms');
return proc;
}
module.exports = { wasmProcess };
|