diff --git a/packages/@jsii/java-runtime-test/project/src/test/java/software/amazon/jsii/testing/ComplianceTest.java b/packages/@jsii/java-runtime-test/project/src/test/java/software/amazon/jsii/testing/ComplianceTest.java index 673397e3ef..ada75214ac 100644 --- a/packages/@jsii/java-runtime-test/project/src/test/java/software/amazon/jsii/testing/ComplianceTest.java +++ b/packages/@jsii/java-runtime-test/project/src/test/java/software/amazon/jsii/testing/ComplianceTest.java @@ -334,8 +334,11 @@ public void exceptions() { calc3.add(3); assertEquals(23, calc3.getValue()); boolean thrown = false; - try { calc3.add(10); } - catch (Exception e) { thrown = true; } + try { + calc3.add(10); + } catch (RuntimeException e) { + thrown = true; + } assertTrue(thrown); calc3.setMaxValue(40); calc3.add(10); @@ -449,7 +452,7 @@ public java.lang.Number overrideMe(java.lang.Number mult) { boolean thrown = false; try { obj.callMe(); - } catch (JsiiException e) { + } catch (RuntimeException e) { assertTrue(e.getMessage().contains( "Thrown by native code")); thrown = true; } @@ -518,7 +521,7 @@ public String getTheProperty() { boolean thrown = false; try { so.retrieveValueOfTheProperty(); - } catch (Exception e) { + } catch (RuntimeException e) { assertTrue(e.getMessage().contains("Oh no, this is bad")); thrown = true; } @@ -536,7 +539,7 @@ public void setTheProperty(String value) { boolean thrown = false; try { so.modifyValueOfTheProperty("Hii"); - } catch (Exception e) { + } catch (RuntimeException e) { assertTrue(e.getMessage().contains("Exception from overloaded setter")); thrown = true; } diff --git a/packages/@jsii/java-runtime/BundledRuntime.t.js b/packages/@jsii/java-runtime/BundledRuntime.t.js index 5bc380d0fd..daf6890ddd 100644 --- a/packages/@jsii/java-runtime/BundledRuntime.t.js +++ b/packages/@jsii/java-runtime/BundledRuntime.t.js @@ -34,7 +34,7 @@ ${resources.map(extractResource).map(indent(12)).join('\n')} return entrypoint.toString(); } catch (final IOException ioe) { - throw new JsiiException("Unable to extract bundled @jsii/runtime library", ioe); + throw new JsiiError("Unable to extract bundled @jsii/runtime library", ioe); } } diff --git a/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiClient.java b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiClient.java index b730bcfa63..9dbbad425d 100644 --- a/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiClient.java +++ b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiClient.java @@ -59,7 +59,7 @@ public void loadModule(final JsiiModule module) { Files.delete(tarball.getParent()); } } catch (IOException e) { - throw new JsiiException("Unable to extract resource " + module.getBundleResourceName(), e); + throw new JsiiError("Unable to extract resource " + module.getBundleResourceName(), e); } } @@ -226,7 +226,7 @@ public List pendingCallbacks() { JsonNode callbacksResp = resp.get("callbacks"); if (callbacksResp == null || !callbacksResp.isArray()) { - throw new JsiiException("Expecting a 'callbacks' key with an array in response"); + throw new JsiiError("Expecting a 'callbacks' key with an array in response"); } ArrayNode callbacksArray = (ArrayNode) callbacksResp; diff --git a/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiEngine.java b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiEngine.java index 225a98a136..c4222013b4 100644 --- a/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiEngine.java +++ b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiEngine.java @@ -164,7 +164,7 @@ private JsiiEngine() { */ public void loadModule(final Class moduleClass) { if (!JsiiModule.class.isAssignableFrom(moduleClass)) { - throw new JsiiException("Invalid module class " + throw new JsiiError("Invalid module class " + moduleClass.getName() + ". It must be derived from JsiiModule"); } @@ -173,7 +173,7 @@ public void loadModule(final Class moduleClass) { try { module = moduleClass.getConstructor().newInstance(); } catch (IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchMethodException e) { - throw new JsiiException(e); + throw new JsiiError(e); } if (this.loadedModules.containsKey(module.getModuleName())) { @@ -270,12 +270,12 @@ public JsiiObjectRef nativeToObjRef(final Object nativeObject) { * * @param objRef The object reference * @return a JsiiObject - * @throws JsiiException If the object is not found. + * @throws JsiiError If the object is not found. */ public Object getObject(final JsiiObjectRef objRef) { Object obj = this.objects.get(objRef.getObjId()); if (obj == null) { - throw new JsiiException("Cannot find jsii object: " + objRef.getObjId()); + throw new JsiiError("Cannot find jsii object: " + objRef.getObjId()); } return obj; } @@ -303,24 +303,24 @@ Class resolveJavaClass(final String fqn) { } String[] parts = fqn.split("\\."); if (parts.length < 2) { - throw new JsiiException("Malformed FQN: " + fqn); + throw new JsiiError("Malformed FQN: " + fqn); } String moduleName = parts[0]; JsonNode names = this.getClient().getModuleNames(moduleName); if (!names.has("java")) { - throw new JsiiException("No java name for module " + moduleName); + throw new JsiiError("No java name for module " + moduleName); } final JsiiModule module = this.loadedModules.get(moduleName); if (module == null) { - throw new JsiiException("No loaded module is named " + moduleName); + throw new JsiiError("No loaded module is named " + moduleName); } try { return module.resolveClass(fqn); } catch (final ClassNotFoundException cfne) { - throw new JsiiException(cfne); + throw new JsiiError(cfne); } } @@ -346,12 +346,12 @@ private JsiiObject createNativeProxy(final String fqn, final JsiiObjectRef objRe ctor.setAccessible(false); return newObj; } catch (NoSuchMethodException e) { - throw new JsiiException("Cannot create native object of type " + throw new JsiiError("Cannot create native object of type " + klass.getName() + " without a constructor that accepts an InitializationMode argument", e); } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) { - throw new JsiiException("Unable to instantiate a new object for FQN " + fqn + ": " + throw new JsiiError("Unable to instantiate a new object for FQN " + fqn + ": " + e.getMessage(), e); } } catch (ClassNotFoundException e) { @@ -369,7 +369,7 @@ private JsiiObject createNativeProxy(final String fqn, final JsiiObjectRef objRe public Enum findEnumValue(final String enumRef) { int sep = enumRef.lastIndexOf('/'); if (sep == -1) { - throw new JsiiException("Malformed enum reference: " + enumRef); + throw new JsiiError("Malformed enum reference: " + enumRef); } String typeName = enumRef.substring(0, sep); @@ -396,7 +396,7 @@ public void processAllPendingCallbacks() { * Invokes a local callback and returns the result/error. * @param callback The callback to invoke. * @return The return value - * @throws JsiiException if the callback failed. + * @throws JsiiError if the callback failed. */ public JsonNode handleCallback(final Callback callback) { @@ -408,7 +408,7 @@ public JsonNode handleCallback(final Callback callback) { return invokeCallbackSet(callback.getSet()); } - throw new JsiiException("Unrecognized callback type: get/set/invoke"); + throw new JsiiError("Unrecognized callback type: get/set/invoke"); } /** @@ -486,9 +486,9 @@ private Object invokeMethod(final Object obj, final Method method, final Object. throw e; } } catch (InvocationTargetException e) { - throw new JsiiException(e.getTargetException()); + throw new JsiiError(e.getTargetException()); } catch (IllegalAccessException e) { - throw new JsiiException(e); + throw new JsiiError(e); } finally { // revert accessibility. method.setAccessible(accessibility); @@ -503,7 +503,7 @@ private void processCallback(final Callback callback) { try { JsonNode result = handleCallback(callback); this.getClient().completeCallback(callback, null, result); - } catch (JsiiException e) { + } catch (JsiiError e) { this.getClient().completeCallback(callback, e.getMessage(), null); } } @@ -528,7 +528,7 @@ private Method findCallbackMethod(final Class klass, final String signature) return findCallbackMethod(klass.getSuperclass(), signature); } - throw new JsiiException("Unable to find callback method with signature: " + signature); + throw new JsiiError("Unable to find callback method with signature: " + signature); } /** @@ -536,7 +536,7 @@ private Method findCallbackMethod(final Class klass, final String signature) * @param klass is the type on which the getter is to be searched for * @param methodName is the name of the getter method * @return the found Method - * @throws JsiiException if no such method is found + * @throws JsiiError if no such method is found */ private Method findCallbackGetter(final Class klass, final String methodName) { try { @@ -549,7 +549,7 @@ private Method findCallbackGetter(final Class klass, final String methodName) // Ignored! } } - throw new JsiiException(nsme); + throw new JsiiError(nsme); } } @@ -559,7 +559,7 @@ private Method findCallbackGetter(final Class klass, final String methodName) * @param methodName is the name of the setter method * @param valueType is the type of the argument the setter accepts * @return the found Method - * @throws JsiiException if no such method is found + * @throws JsiiError if no such method is found */ private Method findCallbackSetter(final Class klass, final String methodName, final Class valueType) { try { @@ -572,7 +572,7 @@ private Method findCallbackSetter(final Class klass, final String methodName, // Ignored! } } - throw new JsiiException(nsme); + throw new JsiiError(nsme); } } @@ -731,7 +731,7 @@ static Jsii tryGetJsiiAnnotation(final Class type, final boolean inherited) { String loadModuleForClass(Class nativeClass) { final Jsii jsii = tryGetJsiiAnnotation(nativeClass, true); if (jsii == null) { - throw new JsiiException("Unable to find @Jsii annotation for class"); + throw new JsiiError("Unable to find @Jsii annotation for class"); } this.loadModule(jsii.module()); diff --git a/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiError.java b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiError.java new file mode 100644 index 0000000000..f64d3a2d9c --- /dev/null +++ b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiError.java @@ -0,0 +1,34 @@ +package software.amazon.jsii; + +/** + * A nonrecoverable error from the jsii runtime, + * usually the kernel. + */ +public final class JsiiError extends JsiiException { + public static final long serialVersionUID = 1L; + + /** + * Creates an exception. + * @param message The error message + */ + JsiiError(final String message) { + super(message); + } + + /** + * Creates an exception. + * @param e The error that caused this exception + */ + JsiiError(final Throwable e) { + super(e); + } + + /** + * Creates an exception. + * @param message The error message + * @param e The error that caused this exception + */ + JsiiError(final String message, final Throwable e) { + super(message, e); + } +} diff --git a/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiException.java b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiException.java index 34361bdaa8..92e5d5a305 100644 --- a/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiException.java +++ b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiException.java @@ -1,33 +1,48 @@ package software.amazon.jsii; -/** +/* * An error raised by the jsii runtime. */ -public final class JsiiException extends RuntimeException { +public abstract class JsiiException extends RuntimeException { public static final long serialVersionUID = 1L; - /** - * Creates an exception. - * @param message The error message - */ - JsiiException(final String message) { - super(message); - } + static enum Type { + JSII_FAULT("@jsii/kernel.Fault"), + RUNTIME_EXCEPTION("@jsii/kernel.RuntimeException"); - /** - * Creates an exception. - * @param e The error that caused this exception - */ - JsiiException(final Throwable e) { - super(e); - } + private final String errorType; + + Type(String str) { + this.errorType = str; + } - /** - * Creates an exception. - * @param message The error message - * @param e The error that caused this exception - */ - JsiiException(final String message, final Throwable e) { - super(message, e); + public String toString() { + return this.errorType; + } } + + /** + * Creates an exception. + * @param message The error message + */ + JsiiException(final String message) { + super(message); + } + + /** + * Creates an exception. + * @param e The error that caused this exception + */ + JsiiException(final Throwable e) { + super(e); + } + + /** + * Creates an exception. + * @param message The error message + * @param e The error that caused this exception + */ + JsiiException(final String message, final Throwable e) { + super(message, e); + } } diff --git a/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiObject.java b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiObject.java index 776b1bbf7a..84761907ec 100644 --- a/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiObject.java +++ b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiObject.java @@ -53,7 +53,7 @@ protected JsiiObject(final InitializationMode initializationMode) { JsiiObject(@Nullable final JsiiEngine engine, final InitializationMode initializationMode) { this.jsii$engine = JsiiEngine.getEngineFor(this, engine); if (initializationMode != InitializationMode.JSII) { - throw new JsiiException("The only supported initialization mode is '" + InitializationMode.JSII + "'"); + throw new JsiiError("The only supported initialization mode is '" + InitializationMode.JSII + "'"); } } @@ -380,11 +380,11 @@ final T asInterfaceProxy(final Class proxyCl constructor.setAccessible(oldAccessible); } } catch(final NoSuchMethodException nsme) { - throw new JsiiException("Unable to find interface proxy constructor on " + proxyClass.getCanonicalName(), nsme); + throw new JsiiError("Unable to find interface proxy constructor on " + proxyClass.getCanonicalName(), nsme); } catch (final InvocationTargetException | InstantiationException e) { - throw new JsiiException("Unable to initialize interface proxy " + proxyClass.getCanonicalName(), e); + throw new JsiiError("Unable to initialize interface proxy " + proxyClass.getCanonicalName(), e); } catch (final IllegalAccessException iae) { - throw new JsiiException("Unable to invoke constructor of " + proxyClass.getCanonicalName(), iae); + throw new JsiiError("Unable to invoke constructor of " + proxyClass.getCanonicalName(), iae); } } @SuppressWarnings("unchecked") diff --git a/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiObjectMapper.java b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiObjectMapper.java index 2dd7d317c8..c5d58a1acc 100644 --- a/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiObjectMapper.java +++ b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiObjectMapper.java @@ -32,7 +32,7 @@ public final class JsiiObjectMapper { /** * Similar to calling JsiiObjectMapper.INSTANCE.treeToValue, but handles a null JsonNode argument - * well, and throws JsiiException instead of JsonProcessingException. + * well, and throws JsiiError instead of JsonProcessingException. * * @param tree the JSON object to parse * @param valueType the expected type value type @@ -228,7 +228,7 @@ private static final class EnumSerializer extends JsonSerializer { public void serialize(final Enum value, final JsonGenerator gen, final SerializerProvider serializers) throws IOException { Jsii jsii = this.tryGetJsiiAnnotation(value.getClass()); if (jsii == null) { - throw new JsiiException("Cannot serialize non-jsii enums"); + throw new JsiiError("Cannot serialize non-jsii enums"); } else { gen.writeStartObject(); gen.writeStringField(TOKEN_ENUM, jsii.fqn() + "/" + value.toString()); diff --git a/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiObjectRef.java b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiObjectRef.java index 383ead102e..ce218d9872 100644 --- a/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiObjectRef.java +++ b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiObjectRef.java @@ -74,7 +74,7 @@ private JsiiObjectRef(final String objId, final Set interfaces, final Js */ public static JsiiObjectRef parse(final JsonNode objRef) { if (!objRef.has(TOKEN_REF)) { - throw new JsiiException("Malformed object reference. Expecting " + TOKEN_REF); + throw new JsiiError("Malformed object reference. Expecting " + TOKEN_REF); } return new JsiiObjectRef(objRef.get(TOKEN_REF).textValue(), objRef); diff --git a/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiRuntime.java b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiRuntime.java index bfda1b8de9..94520bbcbf 100644 --- a/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiRuntime.java +++ b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiRuntime.java @@ -76,7 +76,8 @@ public final class JsiiRuntime { * * @param request The JSON request * @return The JSON response - * @throws JsiiException If the runtime returns an error response. + * @throws JsiiError If the runtime returns an error response originating from the @jsii/kernel. + * @throws RuntimeException If the runtime returns an error response. */ JsonNode requestResponse(final JsonNode request) { try { @@ -104,24 +105,29 @@ JsonNode requestResponse(final JsonNode request) { return resp.get("ok"); } catch (IOException e) { - throw new JsiiException("Unable to send request to jsii-runtime: " + e.toString(), e); + throw new JsiiError("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. + * and throwing a JsiiError or a RuntimeException. * * @param resp The response * @return Never */ private JsonNode processErrorResponse(final JsonNode resp) { + String errorName = resp.get("name").asText(); String errorMessage = resp.get("error").asText(); if (resp.has("stack")) { errorMessage += "\n" + resp.get("stack").asText(); } - throw new JsiiException(errorMessage); + if (errorName.equals(JsiiException.Type.RUNTIME_EXCEPTION.toString())) { + throw new RuntimeException(errorMessage); + } + + throw new JsiiError(errorMessage); } /** @@ -133,7 +139,7 @@ private JsonNode processErrorResponse(final JsonNode resp) { */ private JsonNode processCallbackResponse(final JsonNode resp) { if (this.callbackHandler == null) { - throw new JsiiException("Cannot process callback since callbackHandler was not set"); + throw new JsiiError("Cannot process callback since callbackHandler was not set"); } Callback callback = JsiiObjectMapper.treeToValue(resp.get("callback"), NativeType.forClass(Callback.class)); @@ -310,7 +316,7 @@ private void handshake() { JsonNode helloResponse = this.readNextResponse(); if (!helloResponse.has("hello")) { - throw new JsiiException("Expecting 'hello' message from jsii-runtime"); + throw new JsiiError("Expecting 'hello' message from jsii-runtime"); } String runtimeVersion = helloResponse.get("hello").asText(); @@ -321,19 +327,19 @@ private void handshake() { * Reads the next response from STDOUT of the child process. * * @return The parsed JSON response. - * @throws JsiiException if we couldn't parse the response. + * @throws JsiiError if we couldn't parse the response. */ JsonNode readNextResponse() { try { String responseLine = this.stdout.readLine(); if (responseLine == null) { - throw new JsiiException("Child process exited unexpectedly!"); + throw new JsiiError("Child process exited unexpectedly!"); } final JsonNode response = JsiiObjectMapper.INSTANCE.readTree(responseLine); JsiiRuntime.notifyInspector(response, MessageInspector.MessageType.Response); return response; } catch (IOException e) { - throw new JsiiException("Unable to read reply from jsii-runtime: " + e.toString(), e); + throw new JsiiError("Unable to read reply from jsii-runtime: " + e.toString(), e); } } @@ -345,7 +351,7 @@ JsonNode readNextResponse() { public JsiiClient getClient() { this.startRuntimeIfNeeded(); if (this.client == null) { - throw new JsiiException("Client not created"); + throw new JsiiError("Client not created"); } return this.client; } @@ -356,13 +362,13 @@ public JsiiClient getClient() { * * @param expectedVersion The version this client expects from the runtime * @param actualVersion The actual version the runtime reports - * @throws JsiiException if versions mismatch + * @throws JsiiError 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 " + throw new JsiiError("Incompatible jsii-runtime version. Expecting " + shortExpectedVersion + ", actual was " + shortActualVersion); } diff --git a/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/UnsafeCast.java b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/UnsafeCast.java index c1b3a570ea..ccc6a5b9eb 100644 --- a/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/UnsafeCast.java +++ b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/UnsafeCast.java @@ -49,11 +49,11 @@ public static T unsafeCast(final JsiiObject value, constructor.setAccessible(oldAccessible); } } catch (final NoSuchMethodException nsme) { - throw new JsiiException(String.format("Unable to find interface proxy constructor on %s", annotation.value().getCanonicalName()), nsme); + throw new JsiiError(String.format("Unable to find interface proxy constructor on %s", annotation.value().getCanonicalName()), nsme); } catch (final InvocationTargetException | InstantiationException e) { - throw new JsiiException(String.format("Unable to initialize interface proxy %s", annotation.value().getCanonicalName()), e); + throw new JsiiError(String.format("Unable to initialize interface proxy %s", annotation.value().getCanonicalName()), e); } catch (final IllegalAccessException iae) { - throw new JsiiException(String.format("Unable to invoke constructor of %s", annotation.value().getCanonicalName()), iae); + throw new JsiiError(String.format("Unable to invoke constructor of %s", annotation.value().getCanonicalName()), iae); } } diff --git a/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/Util.java b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/Util.java index e132ee85aa..7e1c82defe 100644 --- a/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/Util.java +++ b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/Util.java @@ -90,7 +90,7 @@ private static boolean isMatchingGetterPresent(final String getterName, final Cl static String javaPropertyToJSProperty(final Method method) { final String getterSetterMethod = method.getName(); if (!isJavaPropertyMethod(method)) { - throw new JsiiException("Invalid getter/setter method. Must start with get/set"); + throw new JsiiError("Invalid getter/setter method. Must start with get/set"); } String camelCase = getterSetterMethod.substring( @@ -112,7 +112,7 @@ static String javaPropertyToJSProperty(final Method method) { */ static String javaScriptPropertyToJavaPropertyName(final String prefix, final String jsPropertyName) { if (jsPropertyName.isEmpty()) { - throw new JsiiException("jsPropertyName must not be empty"); + throw new JsiiError("jsPropertyName must not be empty"); } StringBuilder sb = new StringBuilder(); diff --git a/packages/@jsii/kernel/src/kernel.ts b/packages/@jsii/kernel/src/kernel.ts index 7426dab9da..5100b490c7 100644 --- a/packages/@jsii/kernel/src/kernel.ts +++ b/packages/@jsii/kernel/src/kernel.ts @@ -15,6 +15,28 @@ import * as onExit from './on-exit'; import * as wire from './serialization'; import * as tar from './tar-cache'; +export const enum JsiiErrorType { + JSII_FAULT = '@jsii/kernel.Fault', + RUNTIME_ERROR = '@jsii/kernel.RuntimeError', +} + +export interface JsiiError extends Error { + readonly name: JsiiErrorType; +} + +export class JsiiFault extends Error implements JsiiError { + public readonly name = JsiiErrorType.JSII_FAULT; + public constructor(message: string) { + super(message); + } +} + +export class RuntimeError extends Error implements JsiiError { + public readonly name = JsiiErrorType.RUNTIME_ERROR; + public constructor(message: string) { + super(message); + } +} export class Kernel { /** * Set to true for verbose debugging. @@ -56,7 +78,7 @@ export class Kernel { this._debug('load', req); if ('assembly' in req) { - throw new Error( + throw new JsiiFault( '`assembly` field is deprecated for "load", use `name`, `version` and `tarball` instead', ); } @@ -70,7 +92,7 @@ export class Kernel { // module exists, verify version const epkg = fs.readJsonSync(path.join(packageDir, 'package.json')); if (epkg.version !== pkgver) { - throw new Error( + throw new JsiiFault( `Multiple versions ${pkgver} and ${epkg.version} of the ` + `package '${pkgname}' cannot be loaded together since this is unsupported by ` + 'some runtime environments', @@ -135,7 +157,9 @@ export class Kernel { `loadAssemblyFromPath(${packageDir})`, ); } catch (e: any) { - throw new Error(`Error for package tarball ${req.tarball}: ${e.message}`); + throw new JsiiFault( + `Error for package tarball ${req.tarball}: ${e.message}`, + ); } // load the module and capture its closure @@ -166,13 +190,15 @@ export class Kernel { const epkg = fs.readJsonSync(path.join(packageDir, 'package.json')); if (!epkg.bin) { - throw new Error('There is no bin scripts defined for this package.'); + throw new JsiiFault( + 'There is no bin scripts defined for this package.', + ); } const scriptPath = epkg.bin[req.script]; if (!epkg.bin) { - throw new Error(`Script with name ${req.script} was not defined.`); + throw new JsiiFault(`Script with name ${req.script} was not defined.`); } const result = cp.spawnSync( @@ -198,7 +224,7 @@ export class Kernel { signal: result.signal, }; } - throw new Error(`Package with name ${req.assembly} was not loaded.`); + throw new JsiiFault(`Package with name ${req.assembly} was not loaded.`); } public create(req: api.CreateRequest): api.CreateResponse { @@ -221,7 +247,7 @@ export class Kernel { const ti = this._typeInfoForProperty(property, fqn); if (!ti.static) { - throw new Error(`property ${symbol} is not static`); + throw new JsiiFault(`property ${symbol} is not static`); } const prototype = this._findSymbol(fqn); @@ -244,11 +270,11 @@ export class Kernel { const ti = this._typeInfoForProperty(property, fqn); if (!ti.static) { - throw new Error(`property ${symbol} is not static`); + throw new JsiiFault(`property ${symbol} is not static`); } if (ti.immutable) { - throw new Error(`static property ${symbol} is readonly`); + throw new JsiiFault(`static property ${symbol} is readonly`); } const prototype = this._findSymbol(fqn); @@ -299,7 +325,7 @@ export class Kernel { const propInfo = this._typeInfoForProperty(req.property, fqn, interfaces); if (propInfo.immutable) { - throw new Error( + throw new JsiiFault( `Cannot set value of immutable property ${req.property} to ${req.value}`, ); } @@ -328,7 +354,7 @@ export class Kernel { // verify this is not an async method if (ti.async) { - throw new Error(`${method} is an async method, use "begin" instead`); + throw new JsiiFault(`${method} is an async method, use "begin" instead`); } const fqn = jsiiTypeFqn(obj); @@ -365,12 +391,12 @@ export class Kernel { const ti = this._typeInfoForMethod(method, fqn); if (!ti.static) { - throw new Error(`${fqn}.${method} is not a static method`); + throw new JsiiFault(`${fqn}.${method} is not a static method`); } // verify this is not an async method if (ti.async) { - throw new Error(`${method} is an async method, use "begin" instead`); + throw new JsiiFault(`${method} is an async method, use "begin" instead`); } const prototype = this._findSymbol(fqn); @@ -404,7 +430,7 @@ export class Kernel { this._debug('begin', objref, method, args); if (this.syncInProgress) { - throw new Error( + throw new JsiiFault( `Cannot invoke async method '${req.objref[TOKEN_REF]}.${req.method}' while sync ${this.syncInProgress} is being processed`, ); } @@ -413,7 +439,7 @@ export class Kernel { // verify this is indeed an async method if (!ti.async) { - throw new Error(`Method ${method} is expected to be an async method`); + throw new JsiiFault(`Method ${method} is expected to be an async method`); } const fqn = jsiiTypeFqn(obj); @@ -448,7 +474,7 @@ export class Kernel { const storedPromise = this.promises.get(promiseid); if (storedPromise == null) { - throw new Error(`Cannot find promise with ID: ${promiseid}`); + throw new JsiiFault(`Cannot find promise with ID: ${promiseid}`); } const { promise, method } = storedPromise; @@ -458,7 +484,7 @@ export class Kernel { this._debug('promise result:', result); } catch (e) { this._debug('promise error:', e); - throw e; + throw new JsiiFault((e as any).message); } return { @@ -497,7 +523,7 @@ export class Kernel { const cb = this.waiting.get(cbid); if (!cb) { - throw new Error(`Callback ${cbid} not found`); + throw new JsiiFault(`Callback ${cbid} not found`); } if (err) { @@ -530,7 +556,9 @@ export class Kernel { const assembly = this._assemblyFor(assemblyName); const targets = assembly.metadata.targets; if (!targets) { - throw new Error(`Unexpected - "targets" for ${assemblyName} is missing!`); + throw new JsiiFault( + `Unexpected - "targets" for ${assemblyName} is missing!`, + ); } return { naming: targets }; @@ -581,12 +609,12 @@ export class Kernel { }; case spec.TypeKind.Interface: - throw new Error( + throw new JsiiFault( `Cannot create an object with an FQN of an interface: ${fqn}`, ); default: - throw new Error(`Unexpected FQN kind: ${fqn}`); + throw new JsiiFault(`Unexpected FQN kind: ${fqn}`); } } @@ -635,10 +663,10 @@ export class Kernel { for (const override of overrides) { if (api.isMethodOverride(override)) { if (api.isPropertyOverride(override)) { - throw new Error(overrideTypeErrorMessage); + throw new JsiiFault(overrideTypeErrorMessage); } if (methods.has(override.method)) { - throw new Error( + throw new JsiiFault( `Duplicate override for method '${override.method}'`, ); } @@ -647,10 +675,10 @@ export class Kernel { this._applyMethodOverride(obj, objref, fqn, interfaces, override); } else if (api.isPropertyOverride(override)) { if (api.isMethodOverride(override)) { - throw new Error(overrideTypeErrorMessage); + throw new JsiiFault(overrideTypeErrorMessage); } if (properties.has(override.property)) { - throw Error( + throw new JsiiFault( `Duplicate override for property '${override.property}'`, ); } @@ -658,7 +686,7 @@ export class Kernel { this._applyPropertyOverride(obj, objref, fqn, interfaces, override); } else { - throw new Error(overrideTypeErrorMessage); + throw new JsiiFault(overrideTypeErrorMessage); } } } @@ -679,7 +707,7 @@ export class Kernel { ) { // error if we can find a method with this name if (this._tryTypeInfoForMethod(override.property, typeFqn, interfaces)) { - throw new Error( + throw new JsiiFault( `Trying to override method '${override.property}' as a property`, ); } @@ -802,7 +830,7 @@ export class Kernel { ) { // error if we can find a property with this name if (this._tryTypeInfoForProperty(override.method, typeFqn, interfaces)) { - throw new Error( + throw new JsiiFault( `Trying to override property '${override.method}' as a method`, ); } @@ -940,7 +968,7 @@ export class Kernel { if (!fn) { fn = instance[methodName]; if (!fn) { - throw new Error(`Cannot find ${methodName} on object`); + throw new JsiiFault(`Cannot find ${methodName} on object`); } } return { ti, obj: instance, fn }; @@ -954,7 +982,7 @@ export class Kernel { // error if args > params if (args.length > params.length && !(method && method.variadic)) { - throw new Error( + throw new JsiiFault( `Too many arguments (method accepts ${params.length} parameters, got ${args.length} arguments)`, ); } @@ -969,7 +997,7 @@ export class Kernel { } // No vararg was provided for (let j = i; j < params.length; j++) { if (!param.optional && params[j] === undefined) { - throw new Error( + throw new JsiiFault( `Unexpected 'undefined' value at index ${ j - i } of variadic argument '${ @@ -979,7 +1007,7 @@ export class Kernel { } } } else if (!param.optional && arg === undefined) { - throw new Error( + throw new JsiiFault( `Not enough arguments. Missing argument for the required parameter '${ param.name }' of type '${spec.describeTypeReference(param.type)}'`, @@ -991,7 +1019,7 @@ export class Kernel { private _assemblyFor(assemblyName: string) { const assembly = this.assemblies.get(assemblyName); if (!assembly) { - throw new Error(`Could not find assembly: ${assemblyName}`); + throw new JsiiFault(`Could not find assembly: ${assemblyName}`); } return assembly; } @@ -1010,7 +1038,7 @@ export class Kernel { curr = curr[name]; } if (!curr) { - throw new Error(`Could not find symbol ${fqn}`); + throw new JsiiFault(`Could not find symbol ${fqn}`); } return curr; } @@ -1021,13 +1049,13 @@ export class Kernel { const assembly = this.assemblies.get(moduleName); if (!assembly) { - throw new Error(`Module '${moduleName}' not found`); + throw new JsiiFault(`Module '${moduleName}' not found`); } const types = assembly.metadata.types ?? {}; const fqnInfo = types[fqn]; if (!fqnInfo) { - throw new Error(`Type '${fqn}' not found`); + throw new JsiiFault(`Type '${fqn}' not found`); } return fqnInfo; @@ -1044,7 +1072,7 @@ export class Kernel { interfaces && interfaces.length > 0 ? ` or interface(s) ${interfaces.join(', ')}` : ''; - throw new Error( + throw new JsiiFault( `Class ${fqn}${addendum} doesn't have a method '${methodName}'`, ); } @@ -1114,7 +1142,7 @@ export class Kernel { properties = interfaceTypeInfo.properties; bases = interfaceTypeInfo.interfaces ?? []; } else { - throw new Error( + throw new JsiiFault( `Type of kind ${typeInfo.kind} does not have properties`, ); } @@ -1148,7 +1176,7 @@ export class Kernel { interfaces && interfaces.length > 0 ? ` or interface(s) ${interfaces.join(', ')}` : ''; - throw new Error( + throw new JsiiFault( `Type ${fqn}${addendum} doesn't have a property '${property}'`, ); } @@ -1233,7 +1261,7 @@ export class Kernel { parametersCopy.push(parametersCopy[parametersCopy.length - 1]); } if (xs.length > parametersCopy.length) { - throw new Error( + throw new JsiiFault( `Argument list (${JSON.stringify( xs, )}) not same size as expected argument list (length ${ @@ -1278,6 +1306,14 @@ export class Kernel { this.syncInProgress = desc; try { return fn(); + } catch (e: any) { + if (e.name === JsiiErrorType.JSII_FAULT) { + throw new JsiiFault(e); + } + // This error can be thrown by the kernel directly, or it can be + // thrown from user code. If the error comes from the kernel, then its name field will be populated; + // if the error comes from user code, the name field will not be populated. + throw new RuntimeError(e); } finally { delete this.syncInProgress; } diff --git a/packages/@jsii/kernel/src/objects.ts b/packages/@jsii/kernel/src/objects.ts index 293732ea48..425420a3af 100644 --- a/packages/@jsii/kernel/src/objects.ts +++ b/packages/@jsii/kernel/src/objects.ts @@ -1,6 +1,7 @@ import * as spec from '@jsii/spec'; import * as api from './api'; +import { JsiiFault } from './kernel'; import { EMPTY_OBJECT_FQN } from './serialization'; /** @@ -136,7 +137,7 @@ export class ObjectTable { interfaces?: string[], ): api.ObjRef { if (fqn === undefined) { - throw new Error('FQN cannot be undefined'); + throw new JsiiFault('FQN cannot be undefined'); } const existingRef = objectReference(obj); @@ -179,13 +180,15 @@ export class ObjectTable { */ public findObject(objref: api.ObjRef): RegisteredObject { if (typeof objref !== 'object' || !(api.TOKEN_REF in objref)) { - throw new Error(`Malformed object reference: ${JSON.stringify(objref)}`); + throw new JsiiFault( + `Malformed object reference: ${JSON.stringify(objref)}`, + ); } const objid = objref[api.TOKEN_REF]; const obj = this.objects.get(objid); if (!obj) { - throw new Error(`Object ${objid} not found`); + throw new JsiiFault(`Object ${objid} not found`); } // If there are "additional" interfaces declared on the objref, merge them @@ -215,7 +218,7 @@ export class ObjectTable { */ public deleteObject({ [api.TOKEN_REF]: objid }: api.ObjRef) { if (!this.objects.delete(objid)) { - throw new Error(`Object ${objid} not found`); + throw new JsiiFault(`Object ${objid} not found`); } } @@ -267,7 +270,7 @@ class InterfaceCollection implements Iterable { public addFromClass(fqn: string): void { const ti = this.resolveType(fqn); if (!spec.isClassType(ti)) { - throw new Error( + throw new JsiiFault( `Expected a class, but received ${spec.describeTypeReference(ti)}`, ); } @@ -288,7 +291,7 @@ class InterfaceCollection implements Iterable { public addFromInterface(fqn: string): void { const ti = this.resolveType(fqn); if (!spec.isInterfaceType(ti)) { - throw new Error( + throw new JsiiFault( `Expected an interface, but received ${spec.describeTypeReference(ti)}`, ); } diff --git a/packages/@jsii/kernel/src/recording.ts b/packages/@jsii/kernel/src/recording.ts index 0f77519403..bb77e9c05c 100644 --- a/packages/@jsii/kernel/src/recording.ts +++ b/packages/@jsii/kernel/src/recording.ts @@ -1,6 +1,6 @@ import * as fs from 'fs-extra'; -import { Kernel } from './kernel'; +import { RuntimeError, JsiiErrorType, JsiiFault, Kernel } from './kernel'; export async function closeRecording(kernel: Kernel): Promise { const logfile: fs.WriteStream = (kernel as any).logfile; @@ -50,7 +50,7 @@ export function recordInteraction(kernel: Kernel, inputOutputLogPath: string) { ok(value); }) .catch((err: any) => { - logOutput({ error: err.message }); + logOutput({ error: err.message, name: err.name }); fail(err); }); }); @@ -59,8 +59,11 @@ export function recordInteraction(kernel: Kernel, inputOutputLogPath: string) { logOutput({ ok: ret }); return ret; } catch (e: any) { - logOutput({ error: e.message }); - throw e; + logOutput({ error: e.message, name: e.name }); + if (e.type === JsiiErrorType.RUNTIME_ERROR) { + throw new RuntimeError(e.message); + } + throw new JsiiFault(e.message); } }, }); diff --git a/packages/@jsii/runtime/lib/host.ts b/packages/@jsii/runtime/lib/host.ts index 44a38dec7b..e12d33f418 100644 --- a/packages/@jsii/runtime/lib/host.ts +++ b/packages/@jsii/runtime/lib/host.ts @@ -1,4 +1,4 @@ -import { api, Kernel } from '@jsii/kernel'; +import { api, Kernel, JsiiFault, JsiiError } from '@jsii/kernel'; import { EventEmitter } from 'events'; import { Input, IInputOutput } from './in-out'; @@ -48,7 +48,7 @@ export class KernelHost { function completeCallback(this: KernelHost): void { const req = this.inout.read(); if (!req || 'exit' in req) { - throw new Error('Interrupted before callback returned'); + throw new JsiiFault('Interrupted before callback returned'); } // if this is a completion for the current callback, then we can @@ -59,7 +59,7 @@ export class KernelHost { completeReq.complete.cbid === callback.cbid ) { if (completeReq.complete.err) { - throw new Error(completeReq.complete.err); + throw new JsiiFault(completeReq.complete.err); } return completeReq.complete.result; @@ -93,13 +93,13 @@ export class KernelHost { */ private processRequest(req: Input, next: () => void, sync = false) { if ('callback' in req) { - throw new Error( + throw new JsiiFault( 'Unexpected `callback` result. This request should have been processed by a callback handler', ); } if (!('api' in req)) { - throw new Error('Malformed request, "api" field is required'); + throw new JsiiFault('Malformed request, "api" field is required'); } const apiReq = req; @@ -152,7 +152,7 @@ export class KernelHost { } this.writeOkay(ret); - } catch (e) { + } catch (e: any) { this.writeError(e); } @@ -161,7 +161,7 @@ export class KernelHost { function checkIfAsyncIsAllowed() { if (sync) { - throw new Error( + throw new JsiiFault( 'Cannot handle async operations while waiting for a sync callback to return', ); } @@ -179,11 +179,12 @@ export class KernelHost { /** * Writes an "error" result to stdout. */ - private writeError(error: any) { - const res = { error: error.message, stack: undefined }; - if (!this.opts.noStack) { - res.stack = error.stack; - } + private writeError(error: JsiiError) { + const res = { + error: error.message, + name: error.name, + stack: this.opts.noStack ? undefined : error.stack, + }; this.inout.write(res); }