-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathJsiiRuntime.java
357 lines (299 loc) · 11.4 KB
/
JsiiRuntime.java
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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
package software.amazon.jsii;
import software.amazon.jsii.api.Callback;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.util.stream.Collectors;
import static software.amazon.jsii.JsiiVersion.JSII_RUNTIME_VERSION;
import static software.amazon.jsii.Util.extractResource;
/**
* Manages the jsii-runtime child process.
*/
public final class JsiiRuntime {
/**
* Extract the "+<sha>" postfix from a full version number.
*/
private static final String VERSION_BUILD_PART_REGEX = "\\+[a-z0-9]+$";
/**
* True to print server traces to STDERR.
*/
private static boolean traceEnabled = false;
/**
* The HTTP client connected to this child process.
*/
private JsiiClient client;
/**
* The child procesds.
*/
private Process childProcess;
/**
* Child's standard error.
*/
private BufferedReader stderr;
/**
* Child's standard output.
*/
private BufferedReader stdout;
/**
* Child's standard input.
*/
private BufferedWriter stdin;
/**
* Handler for synchronous callbacks. Must be set using setCallbackHandler.
*/
private JsiiCallbackHandler callbackHandler;
/**
* The main API of this class. Sends a JSON request to jsii-runtime and returns the JSON response.
* @param request The JSON request
* @return The JSON response
* @throws JsiiException If the runtime returns an error response.
*/
JsonNode requestResponse(final JsonNode request) {
try {
// write request
String str = request.toString();
this.stdin.write(str + "\n");
this.stdin.flush();
// read response
JsonNode resp = readNextResponse();
// throw if this is an error response
if (resp.has("error")) {
return processErrorResponse(resp);
}
// process synchronous callbacks (which 'interrupt' the response flow).
if (resp.has("callback")) {
return processCallbackResponse(resp);
}
// null "ok" means undefined result (or void).
return resp.get("ok");
} catch (IOException e) {
throw new JsiiException("Unable to send request to jsii-runtime: " + e.toString(), e);
}
}
/**
* Handles an "error" response by extracting the message and stack trace
* and throwing a JsiiException.
* @param resp The response
* @return Never
*/
private JsonNode processErrorResponse(final JsonNode resp) {
String errorMessage = resp.get("error").asText();
if (resp.has("stack")) {
errorMessage += "\n" + resp.get("stack").asText();
}
throw new JsiiException(errorMessage);
}
/**
* Processes a "callback" response, which is a request to invoke a synchronous callback
* and send back the result.
* @param resp The response.
* @return The next response in the req/res chain.
*/
private JsonNode processCallbackResponse(final JsonNode resp) {
if (this.callbackHandler == null) {
throw new JsiiException("Cannot process callback since callbackHandler was not set");
}
Callback callback = JsiiObjectMapper.treeToValue(resp.get("callback"), Callback.class);
JsonNode result = null;
String error = null;
try {
result = this.callbackHandler.handleCallback(callback);
} catch (Exception e) {
if (e.getCause() instanceof InvocationTargetException) {
error = e.getCause().getCause().getMessage();
} else {
error = e.getMessage();
}
}
ObjectNode completeResponse = JsonNodeFactory.instance.objectNode();
completeResponse.put("cbid", callback.getCbid());
if (error != null) {
completeResponse.put("err", error);
}
if (result != null) {
completeResponse.set("result", result);
}
ObjectNode req = JsonNodeFactory.instance.objectNode();
req.set("complete", completeResponse);
return requestResponse(req);
}
/**
* Sets the handler for sync callbacks.
* @param callbackHandler The handler.
*/
public void setCallbackHandler(final JsiiCallbackHandler callbackHandler) {
this.callbackHandler = callbackHandler;
}
@Override
protected void finalize() throws Throwable {
super.finalize();
if (stderr != null) {
stderr.close();
}
if (stdout != null) {
stdout.close();
}
if (stdin != null) {
stdin.close();
}
}
/**
* Starts jsii-server as a child process if it is not already started.
*/
private void startRuntimeIfNeeded() {
if (childProcess != null) {
return;
}
// If JSII_DEBUG is set, enable traces.
String jsiiDebug = System.getenv("JSII_DEBUG");
if (jsiiDebug != null
&& !jsiiDebug.isEmpty()
&& !jsiiDebug.equalsIgnoreCase("false")
&& !jsiiDebug.equalsIgnoreCase("0")) {
traceEnabled = true;
}
// If JSII_RUNTIME is set, use it to find the jsii-server executable
// otherwise, we default to "jsii-runtime" from PATH.
String jsiiRuntimeExecutable = System.getenv("JSII_RUNTIME");
if (jsiiRuntimeExecutable == null) {
jsiiRuntimeExecutable = prepareBundledRuntime();
}
if (traceEnabled) {
System.err.println("jsii-runtime: " + jsiiRuntimeExecutable);
}
ProcessBuilder pb = new ProcessBuilder("node", jsiiRuntimeExecutable);
if (traceEnabled) {
pb.environment().put("JSII_DEBUG", "1");
}
pb.environment().put("JSII_AGENT", "Java/" + System.getProperty("java.version"));
try {
this.childProcess = pb.start();
} catch (IOException e) {
throw new JsiiException("Cannot find the 'jsii-runtime' executable (JSII_RUNTIME or PATH)");
}
try {
OutputStreamWriter stdinStream = new OutputStreamWriter(this.childProcess.getOutputStream(), "UTF-8");
InputStreamReader stdoutStream = new InputStreamReader(this.childProcess.getInputStream(), "UTF-8");
InputStreamReader stderrStream = new InputStreamReader(this.childProcess.getErrorStream(), "UTF-8");
this.stderr = new BufferedReader(stderrStream);
this.stdout = new BufferedReader(stdoutStream);
this.stdin = new BufferedWriter(stdinStream);
handshake();
this.client = new JsiiClient(this);
// if trace is enabled, start a thread that continuously reads from the child process's
// STDERR and prints to my STDERR.
if (traceEnabled) {
startPipeErrorStreamThread();
}
} catch (IOException e) {
throw new JsiiException(e);
}
}
/**
* Verifies the "hello" message and runtime version compatibility.
* In the meantime, we require full version compatibility, but we should use semver eventually.
*/
private void handshake() {
JsonNode helloResponse = this.readNextResponse();
if (!helloResponse.has("hello")) {
throw new JsiiException("Expecting 'hello' message from jsii-runtime");
}
String runtimeVersion = helloResponse.get("hello").asText();
assertVersionCompatible(JSII_RUNTIME_VERSION, runtimeVersion);
}
/**
* Reads the next response from STDOUT of the child process.
* @return The parsed JSON response.
* @throws JsiiException if we couldn't parse the response.
*/
JsonNode readNextResponse() {
try {
String responseLine = this.stdout.readLine();
if (responseLine == null) {
String error = this.stderr.lines().collect(Collectors.joining("\n\t"));
throw new JsiiException("Child process exited unexpectedly: " + error);
}
return JsiiObjectMapper.INSTANCE.readTree(responseLine);
} catch (IOException e) {
throw new JsiiException("Unable to read reply from jsii-runtime: " + e.toString(), e);
}
}
/**
* Starts a thread that pipes STDERR from the child process to our STDERR.
*/
private void startPipeErrorStreamThread() {
Thread daemon = new Thread(() -> {
while (true) {
try {
String line = stderr.readLine();
System.err.println(line);
if (line == null) {
break;
}
} catch (IOException e) {
e.printStackTrace();
}
}
});
daemon.setDaemon(true);
daemon.start();
}
/**
* This will return the server process in case it is not already started.
* @return A {@link JsiiClient} connected to the server process.
*/
public JsiiClient getClient() {
this.startRuntimeIfNeeded();
if (this.client == null) {
throw new JsiiException("Client not created");
}
return this.client;
}
/**
* Prints jsii-server traces to STDERR.
*/
public static void enableTrace() {
traceEnabled = true;
}
/**
* Asserts that a peer runtimeVersion is compatible with this Java runtime version, which means
* they share the same version components, with the possible exception of the build number.
*
* @param expectedVersion The version this client expects from the runtime
* @param actualVersion The actual version the runtime reports
*
* @throws JsiiException if versions mismatch
*/
static void assertVersionCompatible(final String expectedVersion, final String actualVersion) {
final String shortActualVersion = actualVersion.replaceAll(VERSION_BUILD_PART_REGEX, "");
final String shortExpectedVersion = expectedVersion.replaceAll(VERSION_BUILD_PART_REGEX, "");
if (shortExpectedVersion.compareTo(shortActualVersion) != 0) {
throw new JsiiException("Incompatible jsii-runtime version. Expecting "
+ shortExpectedVersion
+ ", actual was " + shortActualVersion);
}
}
/**
* Extracts all files needed for jsii-runtime.js from JAR into a temp directory.
* @return The full path for jsii-runtime.js
*/
private String prepareBundledRuntime() {
try {
String directory = Files.createTempDirectory("jsii-java-runtime").toString();
String entrypoint = extractResource(getClass(), "jsii-runtime.js", directory);
extractResource(getClass(), "jsii-runtime.js.map", directory);
extractResource(getClass(), "mappings.wasm", directory);
return entrypoint;
} catch (IOException e) {
throw new JsiiException("Unable to extract bundle of jsii-runtime.js from jar", e);
}
}
}