forked from PrairieLearn/PrairieLearn
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathexecutor.js
More file actions
152 lines (137 loc) · 3.98 KB
/
Copy pathexecutor.js
File metadata and controls
152 lines (137 loc) · 3.98 KB
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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
// @ts-check
const readline = require('readline');
const { FunctionMissingError } = require('./lib/code-caller');
const { CodeCallerNative } = require('./lib/code-caller/code-caller-native');
/**
* @typedef {Object} Request
* @property {import('./lib/code-caller/code-caller-native').CallType} type
* @property {string} directory
* @property {string} file
* @property {string} fcn
* @property {any[]} args
*/
/**
* @typedef {Object} Results
* @property {string} [error]
* @property {import('./lib/code-caller/code-caller-native').ErrorData} [errorData]
* @property {any} [data]
* @property {string} [output]
* @property {boolean} [functionMissing]
* @property {boolean} needsFullRestart
*/
/**
* Receives a single line of input and executes the instructions contained in
* it in the provided code caller.
*
* The Promise returned from this function should never reject - errors will
* be indicated by the `error` property on the result.
*
* @param {string} line
* @param {CodeCallerNative} codeCaller
* @returns {Promise<Results>}
*/
async function handleInput(line, codeCaller) {
/** @type {Request} */
let request;
try {
request = JSON.parse(line);
} catch (err) {
// We shouldn't ever get malformed JSON from the caller - but if we do,
// handle it gracefully.
return {
error: err.message,
needsFullRestart: false,
};
}
if (request.fcn === 'restart') {
let restartErr;
let success;
try {
success = await codeCaller.restart();
} catch (err) {
restartErr = err;
}
return {
data: 'success',
needsFullRestart: !!restartErr || !success,
};
}
// Course will always be at `/course` in the Docker executor
try {
await codeCaller.prepareForCourse('/course');
} catch (err) {
// We should never actually hit this case - but if we do, handle it so
// that all our bases are covered.
return { needsFullRestart: true };
}
let result, output, callErr;
try {
({ result, output } = await codeCaller.call(
request.type,
request.directory,
request.file,
request.fcn,
request.args
));
} catch (err) {
callErr = err;
}
const functionMissing = callErr instanceof FunctionMissingError;
return {
// `FunctionMissingError` shouldn't be propagated as an actual error
// we'll report it via `functionMissing`
error: callErr && !functionMissing ? callErr.message : undefined,
errorData: callErr && !functionMissing ? callErr.data : undefined,
data: result,
output,
functionMissing,
needsFullRestart: false,
};
}
// Our overall loop looks like this: read a line of input from stdin, spin
// off a python worker to handle it, and write the results back to stdout.
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false,
});
let questionTimeoutMilliseconds = Number.parseInt(process.env.QUESTION_TIMEOUT_MILLISECONDS);
if (Number.isNaN(questionTimeoutMilliseconds)) {
questionTimeoutMilliseconds = 10000;
}
let codeCaller = new CodeCallerNative({
dropPrivileges: true,
questionTimeoutMilliseconds,
errorLogger: console.error,
});
codeCaller.ensureChild();
// Safety check: if we receive more input while handling another request,
// discard it.
let processingRequest = false;
rl.on('line', (line) => {
if (processingRequest) {
// Someone else messed up - fail fast.
process.exit(1);
}
processingRequest = true;
handleInput(line, codeCaller)
.then((results) => {
const { needsFullRestart, ...rest } = results;
if (needsFullRestart) {
codeCaller.done();
codeCaller = new CodeCallerNative();
codeCaller.ensureChild();
}
console.log(JSON.stringify(rest));
processingRequest = false;
})
.catch((err) => {
console.error(err);
processingRequest = false;
});
});
rl.on('close', () => {
// We can't get any more input; die immediately to allow our container
// to be removed.
process.exit(0);
});