Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions components/framework/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
'use strict';

const spawn = require('cross-spawn');
const YAML = require('js-yaml');
const hasha = require('hasha');
const glob = require('../../src/utils/glob');
const path = require('path');
const spawnExt = require('child-process-ext/spawn');
const spawn = require('../../src/utils/spawn');
const semver = require('semver');
const { configSchema } = require('./configuration');
const ServerlessError = require('../../src/serverless-error');
Expand Down Expand Up @@ -217,7 +216,7 @@ class ServerlessFramework {
) {
let stdoutResult;
try {
const { stdoutBuffer } = await spawnExt('serverless', ['--version']);
const { stdoutBuffer } = await spawn('serverless', ['--version']);
stdoutResult = stdoutBuffer.toString();
} catch (e) {
throw new Error(
Expand Down Expand Up @@ -271,11 +270,16 @@ class ServerlessFramework {

this.context.logVerbose(`Running "${command} ${args.join(' ')}"`);
return new Promise((resolve, reject) => {
const child = spawn(command, args, {
const subprocess = spawn(command, args, {
cwd: this.inputs.path,
stdio: streamStdout ? 'inherit' : undefined,
env: { ...process.env, SLS_DISABLE_AUTO_UPDATE: '1', SLS_COMPOSE: '1' },
});
const child = subprocess.child || subprocess;

if (typeof subprocess.catch === 'function') {
subprocess.catch(() => {});
}

// Make sure that when our process is killed, we terminate the subprocess too
const processExitCallback = () => {
Expand Down
5 changes: 1 addition & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,9 @@
"@aws-sdk/property-provider": "^3.366.0",
"@dagrejs/graphlib": "^3.0.4",
"ajv": "^8.11.0",
"child-process-ext": "^2.1.1",
"ci-info": "^3.3.2",
"cli-cursor": "^3",
"cli-progress-footer": "^2.3.2",
"cross-spawn": "^7.0.3",
"cross-spawn": "^7.0.6",
"d": "^1.0.1",
"event-emitter": "^0.3.5",
"ext": "^1.7.0",
Expand All @@ -52,7 +50,6 @@
"log-node": "^8.0.3",
"memoizee": "^0.4.15",
"minimist": "^1.2.6",
"path2": "^0.1.0",
"ramda": "^0.28.0",
"semver": "^7.3.7",
"signal-exit": "^3.0.7",
Expand Down
2 changes: 1 addition & 1 deletion src/configuration/read.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const { createRequire } = require('module');
const path = require('path');
const fsp = require('fs').promises;
const yaml = require('js-yaml');
const spawn = require('child-process-ext/spawn');
const spawn = require('../utils/spawn');
const ServerlessError = require('../serverless-error');

// Logic for TS resolution is kept as similar as possible to the Serverless Framework codebase
Expand Down
228 changes: 228 additions & 0 deletions src/utils/spawn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
'use strict';

const spawn = require('cross-spawn');
const { PassThrough } = require('stream');

const sensitiveOptionNamePattern =
/(?:^|[-_])(?:auth|authorization|credential|password|passwd|pwd|secret|token|api[-_]?key|access[-_]?key)(?:$|[-_])/i;

const toBuffer = (chunk) => (Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));

const createBufferState = () => ({
buffer: Buffer.alloc(0),
chunks: [],
dirty: false,
length: 0,
});

const appendBuffer = (state, chunk) => {
const buffer = toBuffer(chunk);

state.chunks.push(buffer);
state.length += buffer.length;
state.dirty = true;

return buffer;
};

const getBuffer = (state) => {
if (state.dirty) {
state.buffer = Buffer.concat(state.chunks, state.length);
state.dirty = false;
}

return state.buffer;
};

const redactArgs = (args) => {
const redactedArgs = [];
let redactNext = false;

for (const arg of args) {
const value = String(arg);

if (redactNext) {
redactedArgs.push('<redacted>');
redactNext = false;
continue;
}

const equalsIndex = value.indexOf('=');
const optionName = value.replace(/^-+/, '').split('=')[0];

if (equalsIndex !== -1 && sensitiveOptionNamePattern.test(optionName)) {
redactedArgs.push(`${value.slice(0, equalsIndex + 1)}<redacted>`);
continue;
}

if (value.startsWith('-') && sensitiveOptionNamePattern.test(optionName)) {
redactedArgs.push(value);
redactNext = true;
continue;
}

redactedArgs.push(value);
}

return redactedArgs;
};

module.exports = (command, args = [], options = {}) => {
const normalizedCommand = String(command);
const normalizedArgs = args == null ? [] : Array.from(args, String);
const { shouldCloseStdin, input, ...spawnOptions } = options || {};

const child = spawn(normalizedCommand, normalizedArgs, spawnOptions);
const result = {
child,
stdout: child.stdout || null,
stderr: child.stderr || null,
std: child.stdout || child.stderr ? new PassThrough() : null,
code: undefined,
signal: undefined,
};
if (result.std) result.std.resume();

const stdoutState = createBufferState();
const stderrState = createBufferState();
const stdState = createBufferState();
const outputStreams = [result.stdout, result.stderr].filter(Boolean);
const discardStdData = () => {};
let settled = false;
let waitingForStdDrain = false;
const pausedForStd = new Set();

const resumeStdPausedStreams = () => {
waitingForStdDrain = false;

for (const stream of pausedForStd) {
stream.resume();
}

pausedForStd.clear();
};

const hasActiveStdConsumer = () =>
result.std &&
(result.std.listenerCount('data') > 1 || result.std.listenerCount('readable') > 0);

const pauseForStdBackpressure = () => {
for (const stream of outputStreams) {
if (!stream.isPaused || stream.isPaused()) continue;
stream.pause();
pausedForStd.add(stream);
}

if (!waitingForStdDrain) {
waitingForStdDrain = true;
result.std.once('drain', resumeStdPausedStreams);
}
};

const writeStd = (chunk) => {
if (!result.std || result.std.destroyed || result.std.writableEnded) return;

if (result.std.write(chunk) === false) {
if (hasActiveStdConsumer()) {
pauseForStdBackpressure();
} else {
result.std.resume();
}
}
};

const snapshot = () => ({
child: result.child,
stdout: result.stdout,
stderr: result.stderr,
std: result.std,
stdoutBuffer: getBuffer(stdoutState),
stderrBuffer: getBuffer(stderrState),
stdBuffer: getBuffer(stdState),
code: result.code,
signal: result.signal,
});

const endStd = () => {
if (result.std && !result.std.destroyed && !result.std.writableEnded) {
result.std.end();
}

resumeStdPausedStreams();
};

if (result.std) {
result.std.on('data', discardStdData);
result.std.once('close', resumeStdPausedStreams);
result.std.once('error', resumeStdPausedStreams);
}

if (child.stdout) {
child.stdout.on('data', (chunk) => {
const buffer = appendBuffer(stdoutState, chunk);
appendBuffer(stdState, buffer);
writeStd(buffer);
});
Comment thread
GrahamCampbell marked this conversation as resolved.
}

if (child.stderr) {
child.stderr.on('data', (chunk) => {
const buffer = appendBuffer(stderrState, chunk);
appendBuffer(stdState, buffer);
writeStd(buffer);
});
}

const promise = new Promise((resolve, reject) => {
child.on('error', (error) => {
if (settled) return;
settled = true;
endStd();
const metadata = snapshot();
if (metadata.code === undefined) delete metadata.code;
if (metadata.signal === undefined) delete metadata.signal;
Object.assign(error, metadata);
reject(error);
});

child.on('close', (code, signal) => {
if (settled) return;
settled = true;
result.code = code;
result.signal = signal;
endStd();

if (code === 0) {
resolve(snapshot());
return;
}

const reason = signal ? `signal ${signal}` : `code ${code}`;
const error = new Error(
`\`${[normalizedCommand, ...redactArgs(normalizedArgs)].join(' ')}\` Exited with ${reason}`
);
error.code = code;
error.signal = signal;
Object.assign(error, snapshot());
reject(error);
});

if (input != null && child.stdin) {
child.stdin.end(input);
} else if (shouldCloseStdin && child.stdin) {
child.stdin.end();
}
});

return Object.defineProperties(promise, {
child: { enumerable: true, get: () => result.child },
stdout: { enumerable: true, get: () => result.stdout },
stderr: { enumerable: true, get: () => result.stderr },
std: { enumerable: true, get: () => result.std },
stdoutBuffer: { enumerable: true, get: () => getBuffer(stdoutState) },
stderrBuffer: { enumerable: true, get: () => getBuffer(stderrState) },
stdBuffer: { enumerable: true, get: () => getBuffer(stdState) },
code: { enumerable: true, get: () => result.code },
signal: { enumerable: true, get: () => result.signal },
});
};
Loading