Skip to content

Commit

Permalink
Add object notation support for initializeCommand (#514)
Browse files Browse the repository at this point in the history
* Add object notation support for initializeCommand

* forgot to check in new test config

* dont include commands used for demo
  • Loading branch information
joshspicer committed May 9, 2023
1 parent bdd579a commit 914d873
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 24 deletions.
11 changes: 9 additions & 2 deletions src/spec-common/commonUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,10 @@ export async function runCommand(options: {
resolveOn?: RegExp;
onDidInput?: Event<string>;
stdin?: string;
print?: 'off' | 'continuous' | 'end';
}) {
const { ptyExec, cmd, args, cwd, env, output, resolveOn, onDidInput, stdin } = options;
const print = options.print || 'continuous';

const p = await ptyExec({
cmd,
Expand All @@ -205,18 +207,23 @@ export async function runCommand(options: {

p.onData(chunk => {
cmdOutput += chunk;
output.raw(chunk);
if (print === 'continuous') {
output.raw(chunk);
}
if (resolveOn && resolveOn.exec(cmdOutput)) {
resolve({ cmdOutput });
}
});
p.exit.then(({ code, signal }) => {
try {
if (print === 'end') {
output.raw(cmdOutput);
}
subs.forEach(sub => sub?.dispose());
if (code || signal) {
reject({
message: `Command failed: ${cmd} ${(args || []).join(' ')}`,
cmdOutput: cmdOutput,
cmdOutput,
code,
signal,
});
Expand Down
14 changes: 7 additions & 7 deletions src/spec-common/injectHeadless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ async function runLifecycleCommands(params: ResolverParameters, lifecycleCommand
}
}

async function runLifecycleCommand({ lifecycleHook: postCreate }: ResolverParameters, containerProperties: ContainerProperties, userCommand: LifecycleCommand, userCommandOrigin: string, lifecycleHookName: 'onCreateCommand' | 'updateContentCommand' | 'postCreateCommand' | 'postStartCommand' | 'postAttachCommand', remoteEnv: Promise<Record<string, string>>, secrets: Promise<Record<string, string>>, doRun: boolean) {
async function runLifecycleCommand({ lifecycleHook }: ResolverParameters, containerProperties: ContainerProperties, userCommand: LifecycleCommand, userCommandOrigin: string, lifecycleHookName: 'onCreateCommand' | 'updateContentCommand' | 'postCreateCommand' | 'postStartCommand' | 'postAttachCommand', remoteEnv: Promise<Record<string, string>>, secrets: Promise<Record<string, string>>, doRun: boolean) {
let hasCommand = false;
if (typeof userCommand === 'string') {
hasCommand = userCommand.trim().length > 0;
Expand All @@ -476,17 +476,17 @@ async function runLifecycleCommand({ lifecycleHook: postCreate }: ResolverParame
const progressName = `Running ${lifecycleHookName}...`;
const infoOutput = makeLog({
event(e: LogEvent) {
postCreate.output.event(e);
lifecycleHook.output.event(e);
if (e.type === 'raw' && e.text.includes('::endstep::')) {
postCreate.output.event({
lifecycleHook.output.event({
type: 'progress',
name: progressName,
status: 'running',
stepDetail: ''
});
}
if (e.type === 'raw' && e.text.includes('::step::')) {
postCreate.output.event({
lifecycleHook.output.event({
type: 'progress',
name: progressName,
status: 'running',
Expand All @@ -495,9 +495,9 @@ async function runLifecycleCommand({ lifecycleHook: postCreate }: ResolverParame
}
},
get dimensions() {
return postCreate.output.dimensions;
return lifecycleHook.output.dimensions;
},
onDidChangeDimensions: postCreate.output.onDidChangeDimensions,
onDidChangeDimensions: lifecycleHook.output.onDidChangeDimensions,
}, LogLevel.Info);
try {
const remoteCwd = containerProperties.remoteWorkspaceFolder || containerProperties.homeFolder;
Expand All @@ -515,7 +515,7 @@ async function runLifecycleCommand({ lifecycleHook: postCreate }: ResolverParame
// doesn't get interleaved with the output of other commands.
const printMode = name ? 'off' : 'continuous';
const env = { ...(await remoteEnv), ...(await secrets) };
const { cmdOutput } = await runRemoteCommand({ ...postCreate, output: infoOutput }, containerProperties, typeof postCommand === 'string' ? ['/bin/sh', '-c', postCommand] : postCommand, remoteCwd, { remoteEnv: env, pty: true, print: printMode });
const { cmdOutput } = await runRemoteCommand({ ...lifecycleHook, output: infoOutput }, containerProperties, typeof postCommand === 'string' ? ['/bin/sh', '-c', postCommand] : postCommand, remoteCwd, { remoteEnv: env, pty: true, print: printMode });

// 'name' is set when parallel execution syntax is used.
if (name) {
Expand Down
74 changes: 60 additions & 14 deletions src/spec-node/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { ContainerError, toErrorText } from '../spec-common/errors';
import { CLIHost, runCommandNoPty, runCommand, getLocalUsername } from '../spec-common/commonUtils';
import { Log, LogLevel, makeLog, nullLog } from '../spec-utils/log';

import { ContainerProperties, getContainerProperties, ResolverParameters } from '../spec-common/injectHeadless';
import { ContainerProperties, getContainerProperties, LifecycleCommand, ResolverParameters } from '../spec-common/injectHeadless';
import { Workspace } from '../spec-utils/workspaces';
import { URI } from 'vscode-uri';
import { ShellServer } from '../spec-common/shellServer';
Expand Down Expand Up @@ -408,44 +408,90 @@ export function envListToObj(list: string[] | null | undefined) {
}, {} as Record<string, string>);
}

export async function runInitializeCommand(params: DockerResolverParameters, command: string | string[] | undefined, onDidInput?: Event<string>) {
if (!command) {
export async function runInitializeCommand(params: DockerResolverParameters, userCommand: LifecycleCommand | undefined, onDidInput?: Event<string>) {
if (!userCommand) {
return;
}

let hasCommand = false;
if (typeof userCommand === 'string') {
hasCommand = userCommand.trim().length > 0;
} else if (Array.isArray(userCommand)) {
hasCommand = userCommand.length > 0;
} else if (typeof userCommand === 'object') {
hasCommand = Object.keys(userCommand).length > 0;
}

if (!hasCommand) {
return;
}

const { common, dockerEnv } = params;
const { cliHost, output } = common;
const hookName = 'initializeCommand';
const isWindows = cliHost.platform === 'win32';
const shell = isWindows ? [cliHost.env.ComSpec || 'cmd.exe', '/c'] : ['/bin/sh', '-c'];
const updatedCommand = isWindows && Array.isArray(command) && command.length ?
[(command[0] || '').replace(/\//g, '\\'), ...command.slice(1)] :
command;
const args = typeof updatedCommand === 'string' ? [...shell, updatedCommand] : updatedCommand;
if (!args.length) {
return;
}
const postCommandName = 'initializeCommand';

const infoOutput = makeLog(output, LogLevel.Info);

try {
infoOutput.raw(`\x1b[1mRunning the ${postCommandName} from devcontainer.json...\x1b[0m\r\n\r\n`);
// Runs a command.
// Useful for the object syntax, where >1 command can be specified to run in parallel.
async function runSingleCommand(command: string | string[], name?: string) {
const updatedCommand = isWindows && Array.isArray(command) && command.length ?
[(command[0] || '').replace(/\//g, '\\'), ...command.slice(1)] :
command;
const args = typeof updatedCommand === 'string' ? [...shell, updatedCommand] : updatedCommand;
if (!args.length) {
return;
}

// 'name' is set when parallel execution syntax is used.
if (name) {
infoOutput.raw(`\x1b[1mRunning '${name}' from ${hookName}...\x1b[0m\r\n\r\n`);
} else {
infoOutput.raw(`\x1b[1mRunning the ${hookName} from devcontainer.json...\x1b[0m\r\n\r\n`);
}

// If we have a command name then the command is running in parallel and
// we need to hold output until the command is done so that the output
// doesn't get interleaved with the output of other commands.
const print = name ? 'end' : 'continuous';

await runCommand({
ptyExec: cliHost.ptyExec,
cmd: args[0],
args: args.slice(1),
env: dockerEnv,
output: infoOutput,
onDidInput,
print,
});
infoOutput.raw('\r\n');
}

let commands;
if (typeof userCommand === 'string' || Array.isArray(userCommand)) {
commands = [runSingleCommand(userCommand)];
} else {
commands = Object.keys(userCommand).map(name => {
const command = userCommand[name];
return runSingleCommand(command, name);
});
}
await Promise.all(commands);

} catch (err) {
if (err && (err.code === 130 || err.signal === 2)) { // SIGINT seen on darwin as code === 130, would also make sense as signal === 2.
infoOutput.raw(`\r\n\x1b[1m${postCommandName} interrupted.\x1b[0m\r\n\r\n`);
infoOutput.raw(`\r\n\x1b[1m${hookName} interrupted.\x1b[0m\r\n\r\n`);
} else {
throw new ContainerError({
description: `The ${postCommandName} in the devcontainer.json failed.`,
description: `The ${hookName} in the devcontainer.json failed.`,
originalError: err,
});
}
}

}

export function getFolderImageName(params: ResolverParameters | DockerCLIParameters) {
Expand Down
22 changes: 21 additions & 1 deletion src/test/cli.exec.base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,27 @@ export function describeTests1({ text, options }: BuildKitOption) {
assert.match(res.stdout, /howdy, node/);
});
});

describe(`with valid (image) config and parallel initializeCommand [${text}]`, () => {
let containerId: string | null = null;
const testFolder = `${__dirname}/configs/image-with-parallel-initialize-command`;
beforeEach(async () => containerId = (await devContainerUp(cli, testFolder, options)).containerId);
afterEach(async () => {
await devContainerDown({ containerId });
await shellExec(`rm -f ${testFolder}/*.testMarker`);
});
it('should create testMarker files', async () => {
{
const res = await shellExec(`cat ${testFolder}/initializeCommand.1.testMarker`);
assert.strictEqual(res.error, null);
assert.strictEqual(res.stderr, '');
}
{
const res = await shellExec(`cat ${testFolder}/initializeCommand.2.testMarker`);
assert.strictEqual(res.error, null);
assert.strictEqual(res.stderr, '');
}
});
});
describe(`with valid (Dockerfile) config with target [${text}]`, () => {
let containerId: string | null = null;
const testFolder = `${__dirname}/configs/dockerfile-with-target`;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"image": "mcr.microsoft.com/devcontainers/base:alpine",
"initializeCommand": {
"touch-test1": "touch ${localWorkspaceFolder}/initializeCommand.1.testMarker",
"touch-test2": "touch ${localWorkspaceFolder}/initializeCommand.2.testMarker"
}
}

0 comments on commit 914d873

Please sign in to comment.