aboutsummaryrefslogtreecommitdiff
path: root/editors/vscode/client/vmproc.js
blob: 6b575549eaf49cada0b3b631ff731df6d82213b0 (plain) (blame)
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
// 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 };