Skip to content

Commit c83d4ca

Browse files
chore(toolkit): capture all output from an app (#33259)
### Issue #32997 Relates to #32997 ### Reason for this change When a CDK app is invoked by a sub-shell, we need to capture all output by lines and send it to the IoHost. ### Description of changes Adds an `EventPublisher` interface to the `execInChildProcess` helper. This is getting passed in a publisher that uses the IoHost. Inspired from the [ShellEventPublisher in cdk-assets](https://github.com/cdklabs/cdk-assets/blame/8751d206ea3fa6dbb6156125c64d0ea0e8df289e/lib/private/shell.ts#L6). ### Describe any new or updated permissions being added n/a ### Description of how you validated changes <!--Have you added any unit tests and/or integration tests?--> ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent af44791 commit c83d4ca

File tree

10 files changed

+116
-32
lines changed

10 files changed

+116
-32
lines changed

packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/exec.ts

+23-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import * as child_process from 'node:child_process';
2+
import * as split2 from 'split2';
23
import { ToolkitError } from '../../errors';
34

5+
type EventPublisher = (event: 'open' | 'data_stdout' | 'data_stderr' | 'close', line: string) => void;
6+
47
interface ExecOptions {
8+
eventPublisher?: EventPublisher;
59
extraEnv?: { [key: string]: string | undefined };
610
cwd?: string;
711
}
@@ -17,12 +21,10 @@ export async function execInChildProcess(commandAndArgs: string, options: ExecOp
1721
// number of quoting issues introduced by the intermediate shell layer
1822
// (which would be different between Linux and Windows).
1923
//
20-
// - Inherit stderr from controlling terminal. We don't use the captured value
21-
// anyway, and if the subprocess is printing to it for debugging purposes the
22-
// user gets to see it sooner. Plus, capturing doesn't interact nicely with some
23-
// processes like Maven.
24+
// - We have to capture any output to stdout and stderr sp we can pass it on to the IoHost
25+
// To ensure messages get to the user fast, we will emit every full line we receive.
2426
const proc = child_process.spawn(commandAndArgs, {
25-
stdio: ['ignore', 'inherit', 'inherit'],
27+
stdio: ['ignore', 'pipe', 'pipe'],
2628
detached: false,
2729
shell: true,
2830
cwd: options.cwd,
@@ -32,6 +34,22 @@ export async function execInChildProcess(commandAndArgs: string, options: ExecOp
3234
},
3335
});
3436

37+
const eventPublisher: EventPublisher = options.eventPublisher ?? ((type, line) => {
38+
switch (type) {
39+
case 'data_stdout':
40+
process.stdout.write(line);
41+
return;
42+
case 'data_stderr':
43+
process.stderr.write(line);
44+
return;
45+
case 'open':
46+
case 'close':
47+
return;
48+
}
49+
});
50+
proc.stdout.pipe(split2()).on('data', (line) => eventPublisher('data_stdout', line));
51+
proc.stderr.pipe(split2()).on('data', (line) => eventPublisher('data_stderr', line));
52+
3553
proc.on('error', fail);
3654

3755
proc.on('exit', code => {

packages/@aws-cdk/toolkit/lib/api/cloud-assembly/private/source-builder.ts

+19-6
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ import { assemblyFromDirectory, changeDir, determineOutputDirectory, guessExecut
77
import { ToolkitServices } from '../../../toolkit/private';
88
import { Context, ILock, RWLock, Settings } from '../../aws-cdk';
99
import { ToolkitError } from '../../errors';
10-
import { debug } from '../../io/private';
10+
import { debug, error, info } from '../../io/private';
1111
import { AssemblyBuilder, CdkAppSourceProps } from '../source-builder';
1212

1313
export abstract class CloudAssemblySourceBuilder {
1414
/**
1515
* Helper to provide the CloudAssemblySourceBuilder with required toolkit services
1616
* @deprecated this should move to the toolkit really.
1717
*/
18-
protected abstract toolkitServices(): Promise<ToolkitServices>;
18+
protected abstract sourceBuilderServices(): Promise<ToolkitServices>;
1919

2020
/**
2121
* Create a Cloud Assembly from a Cloud Assembly builder function.
@@ -27,7 +27,7 @@ export abstract class CloudAssemblySourceBuilder {
2727
builder: AssemblyBuilder,
2828
props: CdkAppSourceProps = {},
2929
): Promise<ICloudAssemblySource> {
30-
const services = await this.toolkitServices();
30+
const services = await this.sourceBuilderServices();
3131
const context = new Context({ bag: new Settings(props.context ?? {}) });
3232
const contextAssemblyProps: ContextAwareCloudAssemblyProps = {
3333
services,
@@ -65,7 +65,7 @@ export abstract class CloudAssemblySourceBuilder {
6565
* @returns the CloudAssembly source
6666
*/
6767
public async fromAssemblyDirectory(directory: string): Promise<ICloudAssemblySource> {
68-
const services: ToolkitServices = await this.toolkitServices();
68+
const services: ToolkitServices = await this.sourceBuilderServices();
6969
const contextAssemblyProps: ContextAwareCloudAssemblyProps = {
7070
services,
7171
context: new Context(), // @todo there is probably a difference between contextaware and contextlookup sources
@@ -89,7 +89,7 @@ export abstract class CloudAssemblySourceBuilder {
8989
* @returns the CloudAssembly source
9090
*/
9191
public async fromCdkApp(app: string, props: CdkAppSourceProps = {}): Promise<ICloudAssemblySource> {
92-
const services: ToolkitServices = await this.toolkitServices();
92+
const services: ToolkitServices = await this.sourceBuilderServices();
9393
// @todo this definitely needs to read files from the CWD
9494
const context = new Context({ bag: new Settings(props.context ?? {}) });
9595
const contextAssemblyProps: ContextAwareCloudAssemblyProps = {
@@ -122,7 +122,20 @@ export abstract class CloudAssemblySourceBuilder {
122122

123123
const env = await prepareDefaultEnvironment(services, { outdir });
124124
return await withContext(context.all, env, props.synthOptions, async (envWithContext, _ctx) => {
125-
await execInChildProcess(commandLine.join(' '), { extraEnv: envWithContext, cwd: props.workingDirectory });
125+
await execInChildProcess(commandLine.join(' '), {
126+
eventPublisher: async (type, line) => {
127+
switch (type) {
128+
case 'data_stdout':
129+
await services.ioHost.notify(info(line, 'CDK_ASSEMBLY_I1001'));
130+
break;
131+
case 'data_stderr':
132+
await services.ioHost.notify(error(line, 'CDK_ASSEMBLY_E1002'));
133+
break;
134+
}
135+
},
136+
extraEnv: envWithContext,
137+
cwd: props.workingDirectory,
138+
});
126139
return assemblyFromDirectory(outdir, services.ioHost);
127140
});
128141
} finally {

packages/@aws-cdk/toolkit/lib/api/io/private/codes.ts

+4-18
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,6 @@
11
import { IoMessageCode } from '../io-message';
22

33
export const CODES = {
4-
// Default codes -- all 0000 codes
5-
CDK_TOOLKIT_I0000: 'Default toolkit info code',
6-
CDK_TOOLKIT_E0000: 'Default toolkit error code',
7-
CDK_TOOLKIT_W0000: 'Default toolkit warning code',
8-
CDK_SDK_I0000: 'Default sdk info code',
9-
CDK_SDK_E0000: 'Default sdk error code',
10-
CDK_SDK_WOOOO: 'Default sdk warning code',
11-
CDK_ASSETS_I0000: 'Default assets info code',
12-
CDK_ASSETS_E0000: 'Default assets error code',
13-
CDK_ASSETS_W0000: 'Default assets warning code',
14-
CDK_ASSEMBLY_I0000: 'Default assembly info code',
15-
CDK_ASSEMBLY_E0000: 'Default assembly error code',
16-
CDK_ASSEMBLY_W0000: 'Default assembly warning code',
17-
184
// Toolkit Info codes
195
CDK_TOOLKIT_I0001: 'Display stack data',
206
CDK_TOOLKIT_I0002: 'Successfully deployed stacks',
@@ -31,10 +17,10 @@ export const CODES = {
3117
// Assembly Info codes
3218
CDK_ASSEMBLY_I0042: 'Writing updated context',
3319
CDK_ASSEMBLY_I0241: 'Fetching missing context',
34-
35-
// Assembly Warning codes
36-
37-
// Assembly Error codes
20+
CDK_ASSEMBLY_I1000: 'Cloud assembly output starts',
21+
CDK_ASSEMBLY_I1001: 'Output lines emitted by the cloud assembly to stdout',
22+
CDK_ASSEMBLY_E1002: 'Output lines emitted by the cloud assembly to stderr',
23+
CDK_ASSEMBLY_I1003: 'Cloud assembly output finished',
3824
CDK_ASSEMBLY_E1111: 'Incompatible CDK CLI version. Upgrade needed.',
3925
};
4026

packages/@aws-cdk/toolkit/lib/toolkit/toolkit.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export interface ToolkitOptions {
5656

5757
/**
5858
* Whether to allow ANSI colors and formatting in IoHost messages.
59-
* Setting this value to `falsez enforces that no color or style shows up
59+
* Setting this value to `false` enforces that no color or style shows up
6060
* in messages sent to the IoHost.
6161
* Setting this value to true is a no-op; it is equivalent to the default.
6262
*
@@ -144,9 +144,9 @@ export class Toolkit extends CloudAssemblySourceBuilder implements AsyncDisposab
144144
/**
145145
* Helper to provide the CloudAssemblySourceBuilder with required toolkit services
146146
*/
147-
protected override async toolkitServices(): Promise<ToolkitServices> {
147+
protected override async sourceBuilderServices(): Promise<ToolkitServices> {
148148
return {
149-
ioHost: this.ioHost,
149+
ioHost: withAction(this.ioHost, 'assembly'),
150150
sdkProvider: await this.sdkProvider('assembly'),
151151
};
152152
}

packages/@aws-cdk/toolkit/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"@types/fs-extra": "^9.0.13",
5050
"@types/jest": "^29.5.14",
5151
"@types/node": "^18.18.14",
52+
"@types/split2": "^4.2.3",
5253
"aws-cdk": "0.0.0",
5354
"aws-cdk-lib": "0.0.0",
5455
"aws-sdk-client-mock": "^4.0.1",
@@ -105,6 +106,7 @@
105106
"promptly": "^3.2.0",
106107
"proxy-agent": "^6.4.0",
107108
"semver": "^7.6.3",
109+
"split2": "^4.2.0",
108110
"strip-ansi": "^6.0.1",
109111
"table": "^6.8.2",
110112
"uuid": "^8.3.2",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import * as cdk from 'aws-cdk-lib/core';
2+
3+
console.log('line one');
4+
const app = new cdk.App();
5+
console.log('line two');
6+
new cdk.Stack(app, 'Stack1');
7+
console.log('line three');
8+
app.synth();
9+
console.log('line four');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as cdk from 'aws-cdk-lib/core';
2+
import * as sqs from 'aws-cdk-lib/aws-sqs';
3+
4+
const app = new cdk.App();
5+
const stack = new cdk.Stack(app, 'Stack1');
6+
new sqs.Queue(stack, 'Queue1', {
7+
queueName: "Queue1",
8+
fifo: true,
9+
});
10+
11+
app.synth();

packages/@aws-cdk/toolkit/test/api/cloud-assembly/source-builder.test.ts

+32
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,38 @@ describe('fromCdkApp', () => {
5353
// THEN
5454
expect(JSON.stringify(stack)).toContain('amzn-s3-demo-bucket');
5555
});
56+
57+
test('will capture error output', async () => {
58+
// WHEN
59+
const cx = await appFixture(toolkit, 'validation-error');
60+
try {
61+
await cx.produce();
62+
} catch {
63+
// we are just interested in the output for this test
64+
}
65+
66+
// THEN
67+
expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({
68+
level: 'error',
69+
code: 'CDK_ASSEMBLY_E1002',
70+
message: expect.stringContaining('ValidationError'),
71+
}));
72+
});
73+
74+
test('will capture all output', async () => {
75+
// WHEN
76+
const cx = await appFixture(toolkit, 'console-output');
77+
await cx.produce();
78+
79+
// THEN
80+
['one', 'two', 'three', 'four'].forEach((line) => {
81+
expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({
82+
level: 'info',
83+
code: 'CDK_ASSEMBLY_I1001',
84+
message: `line ${line}`,
85+
}));
86+
});
87+
});
5688
});
5789

5890
describe('fromAssemblyDirectory', () => {

packages/aws-cdk/lib/toolkit/cli-io-host.ts

+1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export const levelPriority: Record<IoMessageLevel, number> = {
7979
* The current action being performed by the CLI. 'none' represents the absence of an action.
8080
*/
8181
export type ToolkitAction =
82+
| 'assembly'
8283
| 'bootstrap'
8384
| 'synth'
8485
| 'list'

yarn.lock

+12
Original file line numberDiff line numberDiff line change
@@ -9422,6 +9422,13 @@
94229422
resolved "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz#5fd3592ff10c1e9695d377020c033116cc2889f2"
94239423
integrity sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==
94249424

9425+
"@types/split2@^4.2.3":
9426+
version "4.2.3"
9427+
resolved "https://registry.npmjs.org/@types/split2/-/split2-4.2.3.tgz#ddd9b6b8518df6e0a7825851fcd98de12e415f0b"
9428+
integrity sha512-59OXIlfUsi2k++H6CHgUQKEb2HKRokUA39HY1i1dS8/AIcqVjtAAFdf8u+HxTWK/4FUHMJQlKSZ4I6irCBJ1Zw==
9429+
dependencies:
9430+
"@types/node" "*"
9431+
94259432
"@types/stack-utils@^2.0.0":
94269433
version "2.0.3"
94279434
resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8"
@@ -19005,6 +19012,11 @@ split2@^3.0.0, split2@^3.2.2:
1900519012
dependencies:
1900619013
readable-stream "^3.0.0"
1900719014

19015+
split2@^4.2.0:
19016+
version "4.2.0"
19017+
resolved "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4"
19018+
integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==
19019+
1900819020
split@^1.0.0, split@^1.0.1:
1900919021
version "1.0.1"
1901019022
resolved "https://registry.npmjs.org/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9"

0 commit comments

Comments
 (0)