Skip to content

Commit b4ef0f0

Browse files
rose-maddaleax
andauthored
fix(cli-repl): ctrl-c terminates running DB operations MONGOSH-640 (#849)
Co-authored-by: Anna Henningsen <[email protected]>
1 parent bbecdd5 commit b4ef0f0

34 files changed

+1064
-121
lines changed

packages/async-rewriter2/README.md

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ they reach their first `await` expression, and the fact that we can determine
3333
which `Promise`s need `await`ing by marking them as such using decorators
3434
on the API surface.
3535

36-
The transformation takes place in two main steps.
36+
The transformation takes place in three main steps.
3737

3838
### Step one: IIFE wrapping
3939

@@ -63,7 +63,64 @@ function foo() {
6363
Note how identifiers remain accessible in the outside environment, including
6464
top-level functions being hoisted to the outside.
6565

66-
### Step two: Async function wrapping
66+
### Step two: Making certain exceptions uncatchable
67+
68+
In order to support Ctrl+C properly, we add a type of exception that is not
69+
catchable by userland code.
70+
71+
For example,
72+
73+
```js
74+
try {
75+
foo3();
76+
} catch {
77+
bar3();
78+
}
79+
```
80+
81+
is transformed into
82+
83+
```js
84+
try {
85+
foo3();
86+
} catch (_err) {
87+
if (!err || !_err[Symbol.for('@@mongosh.uncatchable')]) {
88+
bar3();
89+
} else {
90+
throw _err;
91+
}
92+
}
93+
```
94+
95+
and
96+
97+
```js
98+
try {
99+
foo1();
100+
} catch (err) {
101+
bar1(err);
102+
} finally {
103+
baz();
104+
}
105+
```
106+
107+
into
108+
109+
```js
110+
let _isCatchable;
111+
112+
try {
113+
foo1();
114+
} catch (err) {
115+
_isCatchable = !err || !err[Symbol.for('@@mongosh.uncatchable')];
116+
117+
if (_isCatchable) bar1(err); else throw err;
118+
} finally {
119+
if (_isCatchable) baz();
120+
}
121+
```
122+
123+
### Step three: Async function wrapping
67124

68125
We perform three operations:
69126

packages/async-rewriter2/src/async-writer-babel.spec.ts

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ describe('AsyncWriter', () => {
4949
return Object.assign(
5050
Promise.resolve(implicitlyAsyncValue),
5151
{ [Symbol.for('@@mongosh.syntheticPromise')]: true });
52+
},
53+
throwUncatchable() {
54+
throw Object.assign(
55+
new Error('uncatchable!'),
56+
{ [Symbol.for('@@mongosh.uncatchable')]: true });
5257
}
5358
});
5459
runTranspiledCode = (code: string, context?: any) => {
@@ -760,4 +765,182 @@ describe('AsyncWriter', () => {
760765
});
761766
});
762767
});
768+
769+
context('uncatchable exceptions', () => {
770+
it('allows catching regular exceptions', () => {
771+
const result = runTranspiledCode(`
772+
(() => {
773+
try {
774+
throw new Error('generic error');
775+
} catch (err) {
776+
return ({ caught: err });
777+
}
778+
})();`);
779+
expect(result.caught.message).to.equal('generic error');
780+
});
781+
782+
it('allows catching regular exceptions with destructuring catch (object)', () => {
783+
const result = runTranspiledCode(`
784+
(() => {
785+
try {
786+
throw new Error('generic error');
787+
} catch ({ message }) {
788+
return ({ caught: message });
789+
}
790+
})();`);
791+
expect(result.caught).to.equal('generic error');
792+
});
793+
794+
795+
it('allows catching regular exceptions with destructuring catch (array)', () => {
796+
const result = runTranspiledCode(`
797+
(() => {
798+
try {
799+
throw [ 'foo' ];
800+
} catch ([message]) {
801+
return ({ caught: message });
802+
}
803+
})();`);
804+
expect(result.caught).to.equal('foo');
805+
});
806+
807+
it('allows catching regular exceptions with destructuring catch (assignable)', () => {
808+
const result = runTranspiledCode(`
809+
(() => {
810+
try {
811+
throw [ 'foo' ];
812+
} catch ([message]) {
813+
message = 42;
814+
return ({ caught: message });
815+
}
816+
})();`);
817+
expect(result.caught).to.equal(42);
818+
});
819+
820+
it('allows rethrowing regular exceptions', () => {
821+
try {
822+
runTranspiledCode(`
823+
(() => {
824+
try {
825+
throw new Error('generic error');
826+
} catch (err) {
827+
throw err;
828+
}
829+
})();`);
830+
expect.fail('missed exception');
831+
} catch (err) {
832+
expect(err.message).to.equal('generic error');
833+
}
834+
});
835+
836+
it('allows returning from finally', () => {
837+
const result = runTranspiledCode(`
838+
(() => {
839+
try {
840+
throw new Error('generic error');
841+
} catch (err) {
842+
return ({ caught: err });
843+
} finally {
844+
return 'finally';
845+
}
846+
})();`);
847+
expect(result).to.equal('finally');
848+
});
849+
850+
it('allows finally without catch', () => {
851+
const result = runTranspiledCode(`
852+
(() => {
853+
try {
854+
throw new Error('generic error');
855+
} finally {
856+
return 'finally';
857+
}
858+
})();`);
859+
expect(result).to.equal('finally');
860+
});
861+
862+
it('allows throwing primitives', () => {
863+
const result = runTranspiledCode(`
864+
(() => {
865+
try {
866+
throw null;
867+
} catch (err) {
868+
return ({ caught: err });
869+
}
870+
})();`);
871+
expect(result.caught).to.equal(null);
872+
});
873+
874+
it('allows throwing primitives with finally', () => {
875+
const result = runTranspiledCode(`
876+
(() => {
877+
try {
878+
throw null;
879+
} catch (err) {
880+
return ({ caught: err });
881+
} finally {
882+
return 'finally';
883+
}
884+
})();`);
885+
expect(result).to.equal('finally');
886+
});
887+
888+
it('does not catch uncatchable exceptions', () => {
889+
try {
890+
runTranspiledCode(`
891+
(() => {
892+
try {
893+
throwUncatchable();
894+
} catch (err) {
895+
return ({ caught: err });
896+
}
897+
})();`);
898+
expect.fail('missed exception');
899+
} catch (err) {
900+
expect(err.message).to.equal('uncatchable!');
901+
}
902+
});
903+
904+
it('does not catch uncatchable exceptions with empty catch clause', () => {
905+
try {
906+
runTranspiledCode(`
907+
(() => {
908+
try {
909+
throwUncatchable();
910+
} catch { }
911+
})();`);
912+
expect.fail('missed exception');
913+
} catch (err) {
914+
expect(err.message).to.equal('uncatchable!');
915+
}
916+
});
917+
918+
it('does not catch uncatchable exceptions with finalizer', () => {
919+
try {
920+
runTranspiledCode(`
921+
(() => {
922+
try {
923+
throwUncatchable();
924+
} catch { } finally { return; }
925+
})();`);
926+
expect.fail('missed exception');
927+
} catch (err) {
928+
expect(err.message).to.equal('uncatchable!');
929+
}
930+
});
931+
932+
it('does not catch uncatchable exceptions with only finalizer', () => {
933+
try {
934+
runTranspiledCode(`
935+
(() => {
936+
try {
937+
throwUncatchable();
938+
} finally { return; }
939+
})();`);
940+
expect.fail('missed exception');
941+
} catch (err) {
942+
expect(err.message).to.equal('uncatchable!');
943+
}
944+
});
945+
});
763946
});

packages/async-rewriter2/src/async-writer-babel.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
import * as babel from '@babel/core';
33
import runtimeSupport from './runtime-support.nocov';
44
import wrapAsFunctionPlugin from './stages/wrap-as-iife';
5+
import uncatchableExceptionPlugin from './stages/uncatchable-exceptions';
56
import makeMaybeAsyncFunctionPlugin from './stages/transform-maybe-await';
67
import { AsyncRewriterErrors } from './error-codes';
78

89
/**
910
* General notes for this package:
1011
*
11-
* This package contains two babel plugins used in async rewriting, plus a helper
12+
* This package contains three babel plugins used in async rewriting, plus a helper
1213
* to apply these plugins to plain code.
1314
*
1415
* If you have not worked with babel plugins,
@@ -51,6 +52,7 @@ export default class AsyncWriter {
5152
require('@babel/plugin-transform-destructuring').default
5253
]);
5354
code = this.step(code, [wrapAsFunctionPlugin]);
55+
code = this.step(code, [uncatchableExceptionPlugin]);
5456
code = this.step(code, [
5557
[
5658
makeMaybeAsyncFunctionPlugin,

0 commit comments

Comments
 (0)