Skip to content

Commit 58149b8

Browse files
committed
Switched from exec to spawn to handle large payload responses; improved logging and code style
1 parent 21f9277 commit 58149b8

File tree

2 files changed

+96
-38
lines changed

2 files changed

+96
-38
lines changed

src/docker.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { spawn } from 'child_process';
2+
3+
/**
4+
* Utility function to promisify spawn
5+
*
6+
* @param command The command to run
7+
* @param args The args as an array of strings
8+
*/
9+
const asyncSpawn = (command: string, args: string[]): Promise<{ stdout: string; stderr: string }> => {
10+
return new Promise((resolve, reject) => {
11+
const process = spawn(command, args);
12+
let stdout = '';
13+
let stderr = '';
14+
15+
process.stdout.on('data', (data) => {
16+
stdout += data.toString();
17+
});
18+
19+
process.stderr.on('data', (data) => {
20+
stderr += data.toString();
21+
});
22+
23+
process.on('close', (code) => {
24+
if (code === 0) {
25+
resolve({ stdout, stderr });
26+
} else {
27+
reject(new Error(`Command exited with code ${code}\nSTDOUT: ${stdout}\nSTDERR: ${stderr}`));
28+
}
29+
});
30+
31+
process.on('error', (err) => {
32+
reject(err);
33+
});
34+
});
35+
};
36+
37+
/**
38+
* Runs the Docker Exec command
39+
*
40+
* @param container The docker container name (e.g. 'php')
41+
* @param handler The handler command (e.g. '/path/to/vendor/bref/bref-local handler.php')
42+
* @param payload The JSON-encoded payload
43+
*/
44+
export const runDockerCommand = async (container: string, handler: string, payload: string): Promise<string> => {
45+
// Build the docker command: '/usr/bin/docker exec $CONTAINER $HANDLER $PAYLOAD' for spawn
46+
const [command, ...handlerArgs] = handler.split(' ');
47+
const dockerCommand = [
48+
"exec",
49+
container,
50+
command,
51+
...handlerArgs,
52+
payload,
53+
];
54+
55+
// Run the command and pull the output into a string
56+
let result: string|null = null;
57+
try {
58+
const { stdout, stderr } = await asyncSpawn("/usr/bin/docker", dockerCommand);
59+
if (stderr) {
60+
console.info(`END [DOCKER] COMMAND: `, dockerCommand);
61+
console.error(`END [DOCKER] STDERR: ${stderr}`);
62+
}
63+
result = Buffer.from(stdout).toString();
64+
} catch (error) {
65+
console.info(`END [DOCKER] COMMAND: `, dockerCommand);
66+
console.error(`END [DOCKER] ERROR: ${(error as Error).message}`);
67+
throw error;
68+
}
69+
70+
// Strip header info from bref-local output
71+
if (handler?.includes('bref-local')) {
72+
// The 'bref-local' handler returns the following header which needs to be stripped:
73+
// v
74+
// START
75+
// END Duration ...
76+
//
77+
// ^
78+
// (real output begins under this line)
79+
//
80+
result = result
81+
.split('\n') // Split the output into lines
82+
.slice(3) // Skip the first three lines
83+
.join('\n'); // Join the remaining lines back together
84+
}
85+
86+
return result;
87+
}

src/index.ts

Lines changed: 9 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@ import express, { NextFunction, Request, Response } from 'express';
22
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
33
// @ts-ignore
44
import queue from 'express-queue';
5-
import { promisify } from 'util';
6-
import { exec } from 'child_process';
75
import { APIGatewayProxyStructuredResultV2 } from 'aws-lambda';
86
import { InvocationType, InvokeCommand, InvokeCommandOutput, LambdaClient } from '@aws-sdk/client-lambda';
97
import { httpRequestToEvent } from './apiGateway.js';
8+
import { runDockerCommand } from "./docker";
109
import bodyParser from 'body-parser';
1110

1211
const app = express();
@@ -30,7 +29,7 @@ const doDebugLogging = process.env.LOG_LEVEL === 'debug';
3029

3130
// Determine whether to use Docker CLI or AWS Lambda RIE for execution
3231
const dockerHost = process.env.TARGET_CONTAINER ?? target.split(":")[0];
33-
const dockerHandler = process.env.TARGET_HANDLER;
32+
const dockerHandler = process.env.TARGET_HANDLER ?? '';
3433
const mode = (dockerHost && dockerHandler) ? "docker": "rie";
3534
if (doInfoLogging) {
3635
if (mode === "docker") {
@@ -39,7 +38,6 @@ if (doInfoLogging) {
3938
console.log("Using AWS Lambda RIE environment - set TARGET_CONTAINER and TARGET_HANDLER environment variables to enable docker CLI mode");
4039
}
4140
}
42-
const isBrefLocalHandler = dockerHandler?.includes('bref-local') ?? false;
4341
const maxParallelRequests = process.env.DEV_MAX_REQUESTS_IN_PARALLEL ?? 10;
4442
const maxQueuedRequests = process.env.DEV_MAX_REQUESTS_IN_QUEUE ?? -1;
4543

@@ -62,9 +60,6 @@ const requestQueue = queue({
6260
});
6361
app.use(requestQueue);
6462

65-
// Create an async version of exec() for calling Docker
66-
const asyncExec = promisify(exec);
67-
6863
const client = new LambdaClient({
6964
region: 'us-east-1',
7065
endpoint: `http://${target}`,
@@ -92,47 +87,23 @@ app.all('*', async (req: Request, res: Response, next) => {
9287
try {
9388
const payload = Buffer.from(JSON.stringify(event)).toString();
9489
if (doInfoLogging) {
95-
console.log(`START [${mode.toUpperCase()}] ${requestContext?.method} ${requestContext?.path}`, doDebugLogging ? payload : null);
90+
console.log(`START [${mode.toUpperCase()}] ${requestContext?.method} ${requestContext?.path}`, doDebugLogging ? payload : '');
9691
}
97-
9892
if (mode === "docker") {
99-
const payloadAsEscapedJson = payload.replace("'", "\\'");
100-
const dockerCommand = `/usr/bin/docker exec ${dockerHost} ${dockerHandler} '${payloadAsEscapedJson}'`;
101-
const {stdout, stderr} = await asyncExec(dockerCommand);
102-
result = Buffer.from(stdout).toString();
103-
104-
if (isBrefLocalHandler) {
105-
// The 'bref-local' handler returns the following, which needs to be stripped:
106-
// START
107-
// END Duration XXXXX
108-
// (blank line)
109-
// ...real output...
110-
result = result
111-
.split('\n') // Split the output into lines
112-
.slice(3) // Skip the first three lines
113-
.join('\n'); // Join the remaining lines back together
114-
}
115-
if (doInfoLogging) {
116-
console.log(`END [DOCKER] ${requestContext?.method} ${requestContext?.path}`, doDebugLogging ? result : null);
117-
if (doDebugLogging) {
118-
console.log(`END [DOCKER] CMD `, dockerCommand);
119-
console.log(`END [DOCKER] STDOUT`, stdout);
120-
if (stderr) {
121-
console.error(`END [DOCKER] STDERR: ${stderr}`);
122-
}
123-
}
124-
}
93+
// Run via Docker
94+
result = await runDockerCommand(dockerHost, dockerHandler, payload);
12595
} else {
96+
// Run via Lambda RIE SDK
12697
const invokeCommand = new InvokeCommand({
12798
FunctionName: 'function',
12899
Payload: payload,
129100
InvocationType: InvocationType.RequestResponse,
130101
});
131102
const invokeResponse: InvokeCommandOutput = await client.send(invokeCommand);
132103
result = String(invokeResponse.Payload);
133-
if (doDebugLogging) {
134-
console.log(`END [RIE] ${requestContext?.method} ${requestContext?.path}`, doDebugLogging ? result : null);
135-
}
104+
}
105+
if (doInfoLogging) {
106+
console.log(`END [${mode.toUpperCase()}] ${requestContext?.method} ${requestContext?.path}`, doDebugLogging ? result : `${result.length} bytes`);
136107
}
137108

138109
} catch (e) {

0 commit comments

Comments
 (0)