Skip to content

Publish without node-pty dependency #585

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 13, 2023
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
11 changes: 5 additions & 6 deletions build/patch-packagejson.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@ const path = require('path');

const packageJsonPath = path.join(__dirname, '..', 'package.json');
const packageJsonText = fs.readFileSync(packageJsonPath, 'utf8');
const packageJson = jsonc.parse(packageJsonText);

const dependencies = {
'node-pty': packageJson.dependencies['node-pty'],
};
let patchedText = packageJsonText;
for (const key of ['dependencies', 'devDependencies']) {
const edits = jsonc.modify(patchedText, [key], {}, {});
patchedText = jsonc.applyEdits(patchedText, edits);
}

const edits = jsonc.modify(packageJsonText, ['dependencies'], dependencies, {});
const patchedText = jsonc.applyEdits(packageJsonText, edits);
fs.writeFileSync(packageJsonPath, patchedText);
4 changes: 2 additions & 2 deletions src/spec-common/cliHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ export enum FileTypeBitmask {
SymbolicLink = 64
}

export async function getCLIHost(localCwd: string, loadNativeModule: <T>(moduleName: string) => Promise<T | undefined>): Promise<CLIHost> {
export async function getCLIHost(localCwd: string, loadNativeModule: <T>(moduleName: string) => Promise<T | undefined>, allowInheritTTY: boolean): Promise<CLIHost> {
const exec = plainExec(localCwd);
const ptyExec = await plainPtyExec(localCwd, loadNativeModule);
const ptyExec = await plainPtyExec(localCwd, loadNativeModule, allowInheritTTY);
return createLocalCLIHostFromExecFunctions(localCwd, exec, ptyExec, connectLocal);
}

Expand Down
56 changes: 51 additions & 5 deletions src/spec-common/commonUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import * as ptyType from 'node-pty';
import { StringDecoder } from 'string_decoder';

import { toErrorText } from './errors';
import { Disposable, Event } from '../spec-utils/event';
import { Disposable, Event, NodeEventEmitter } from '../spec-utils/event';
import { isLocalFile } from '../spec-utils/pfs';
import { Log, nullLog } from '../spec-utils/log';
import { ShellServer } from './shellServer';
Expand All @@ -32,6 +32,7 @@ export interface ExecParameters {
cwd?: string;
cmd: string;
args?: string[];
stdio?: [cp.StdioNull | cp.StdioPipe, cp.StdioNull | cp.StdioPipe, cp.StdioNull | cp.StdioPipe];
output: Log;
}

Expand Down Expand Up @@ -284,15 +285,15 @@ export const processSignals: Record<string, number | undefined> = {

export function plainExec(defaultCwd: string | undefined): ExecFunction {
return async function (params: ExecParameters): Promise<Exec> {
const { cmd, args, output } = params;
const { cmd, args, stdio, output } = params;

const text = `Run: ${cmd} ${(args || []).join(' ').replace(/\n.*/g, '')}`;
const start = output.start(text);

const cwd = params.cwd || defaultCwd;
const env = params.env ? { ...process.env, ...params.env } : process.env;
const exec = await findLocalWindowsExecutable(cmd, cwd, env, output);
const p = cp.spawn(exec, args, { cwd, env, windowsHide: true });
const p = cp.spawn(exec, args, { cwd, env, stdio: stdio as any, windowsHide: true });

return {
stdin: p.stdin,
Expand All @@ -315,10 +316,11 @@ export function plainExec(defaultCwd: string | undefined): ExecFunction {
};
}

export async function plainPtyExec(defaultCwd: string | undefined, loadNativeModule: <T>(moduleName: string) => Promise<T | undefined>): Promise<PtyExecFunction> {
export async function plainPtyExec(defaultCwd: string | undefined, loadNativeModule: <T>(moduleName: string) => Promise<T | undefined>, allowInheritTTY: boolean): Promise<PtyExecFunction> {
const pty = await loadNativeModule<typeof ptyType>('node-pty');
if (!pty) {
throw new Error('Missing node-pty');
const plain = plainExec(defaultCwd);
return plainExecAsPtyExec(plain, allowInheritTTY);
}

return async function (params: PtyExecParameters): Promise<PtyExec> {
Expand Down Expand Up @@ -368,6 +370,50 @@ export async function plainPtyExec(defaultCwd: string | undefined, loadNativeMod
};
}

export function plainExecAsPtyExec(plain: ExecFunction, allowInheritTTY: boolean): PtyExecFunction {
return async function (params: PtyExecParameters): Promise<PtyExec> {
const p = await plain({
...params,
stdio: allowInheritTTY && params.output !== nullLog ? [
process.stdin.isTTY ? 'inherit' : 'pipe',
process.stdout.isTTY ? 'inherit' : 'pipe',
process.stderr.isTTY ? 'inherit' : 'pipe',
] : undefined,
});
const onDataEmitter = new NodeEventEmitter<string>();
if (p.stdout) {
const stdoutDecoder = new StringDecoder();
p.stdout.on('data', data => onDataEmitter.fire(stdoutDecoder.write(data)));
p.stdout.on('close', () => {
const end = stdoutDecoder.end();
if (end) {
onDataEmitter.fire(end);
}
});
}
if (p.stderr) {
const stderrDecoder = new StringDecoder();
p.stderr.on('data', data => onDataEmitter.fire(stderrDecoder.write(data)));
p.stderr.on('close', () => {
const end = stderrDecoder.end();
if (end) {
onDataEmitter.fire(end);
}
});
}
return {
onData: onDataEmitter.event,
write: p.stdin ? p.stdin.write.bind(p.stdin) : () => {},
resize: () => {},
exit: p.exit.then(({ code, signal }) => ({
code: typeof code === 'number' ? code : undefined,
signal: typeof signal === 'string' ? processSignals[signal] : undefined,
})),
terminate: p.terminate.bind(p),
};
};
}

async function findLocalWindowsExecutable(command: string, cwd = process.cwd(), env: Record<string, string | undefined>, output: Log): Promise<string> {
if (process.platform !== 'win32') {
return command;
Expand Down
1 change: 1 addition & 0 deletions src/spec-common/injectHeadless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export interface ResolverParameters {
getLogLevel: () => LogLevel;
onDidChangeLogLevel: Event<LogLevel>;
loadNativeModule: <T>(moduleName: string) => Promise<T | undefined>;
allowInheritTTY: boolean;
shutdowns: (() => Promise<void>)[];
backgroundTasks: (Promise<void> | (() => Promise<void>))[];
persistedFolder: string; // A path where config can be persisted and restored at a later time. Should default to tmpdir() folder if not provided.
Expand Down
4 changes: 3 additions & 1 deletion src/spec-node/devContainers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ export async function createDockerParams(options: ProvisionOptions, disposables:

const appRoot = undefined;
const cwd = options.workspaceFolder || process.cwd();
const cliHost = await getCLIHost(cwd, loadNativeModule);
const allowInheritTTY = options.logFormat === 'text';
const cliHost = await getCLIHost(cwd, loadNativeModule, allowInheritTTY);
const sessionId = crypto.randomUUID();

const common: ResolverParameters = {
Expand All @@ -130,6 +131,7 @@ export async function createDockerParams(options: ProvisionOptions, disposables:
getLogLevel: () => options.logLevel,
onDidChangeLogLevel: () => ({ dispose() { } }),
loadNativeModule,
allowInheritTTY,
shutdowns: [],
backgroundTasks: [],
persistedFolder: persistedFolder || await getCacheFolder(cliHost), // Fallback to tmp folder, even though that isn't 'persistent'
Expand Down
8 changes: 4 additions & 4 deletions src/spec-node/devContainersSpecCLI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ async function provision({
const providedIdLabels = idLabel ? Array.isArray(idLabel) ? idLabel as string[] : [idLabel] : undefined;

const cwd = workspaceFolder || process.cwd();
const cliHost = await getCLIHost(cwd, loadNativeModule);
const cliHost = await getCLIHost(cwd, loadNativeModule, logFormat === 'text');
const secretsP = readSecretsFromFile({ secretsFile, cliHost });

const options: ProvisionOptions = {
Expand Down Expand Up @@ -786,7 +786,7 @@ async function doRunUserCommands({
const overrideConfigFile = overrideConfig ? URI.file(path.resolve(process.cwd(), overrideConfig)) : undefined;

const cwd = workspaceFolder || process.cwd();
const cliHost = await getCLIHost(cwd, loadNativeModule);
const cliHost = await getCLIHost(cwd, loadNativeModule, logFormat === 'text');
const secretsP = readSecretsFromFile({ secretsFile, cliHost });

const params = await createDockerParams({
Expand Down Expand Up @@ -954,7 +954,7 @@ async function readConfiguration({
const configFile = configParam ? URI.file(path.resolve(process.cwd(), configParam)) : undefined;
const overrideConfigFile = overrideConfig ? URI.file(path.resolve(process.cwd(), overrideConfig)) : undefined;
const cwd = workspaceFolder || process.cwd();
const cliHost = await getCLIHost(cwd, loadNativeModule);
const cliHost = await getCLIHost(cwd, loadNativeModule, logFormat === 'text');
const extensionPath = path.join(__dirname, '..', '..');
const sessionStart = new Date();
const pkg = getPackageConfig();
Expand Down Expand Up @@ -1082,7 +1082,7 @@ async function outdated({
try {
const workspaceFolder = path.resolve(process.cwd(), workspaceFolderArg);
const configFile = configParam ? URI.file(path.resolve(process.cwd(), configParam)) : undefined;
const cliHost = await getCLIHost(workspaceFolder, loadNativeModule);
const cliHost = await getCLIHost(workspaceFolder, loadNativeModule, logFormat === 'text');
const extensionPath = path.join(__dirname, '..', '..');
const sessionStart = new Date();
const pkg = getPackageConfig();
Expand Down
2 changes: 1 addition & 1 deletion src/spec-node/featuresCLI/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ async function featuresPackage({
const pkg = getPackageConfig();

const cwd = process.cwd();
const cliHost = await getCLIHost(cwd, loadNativeModule);
const cliHost = await getCLIHost(cwd, loadNativeModule, true);
const output = createLog({
logLevel: mapLogLevel(inputLogLevel),
logFormat: 'text',
Expand Down
2 changes: 1 addition & 1 deletion src/spec-node/featuresCLI/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ async function featuresPublish({
const pkg = getPackageConfig();

const cwd = process.cwd();
const cliHost = await getCLIHost(cwd, loadNativeModule);
const cliHost = await getCLIHost(cwd, loadNativeModule, true);
const output = createLog({
logLevel: mapLogLevel(inputLogLevel),
logFormat: 'text',
Expand Down
2 changes: 1 addition & 1 deletion src/spec-node/featuresCLI/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ async function featuresTest({
};

const cwd = process.cwd();
const cliHost = await getCLIHost(cwd, loadNativeModule);
const cliHost = await getCLIHost(cwd, loadNativeModule, true);
const pkg = getPackageConfig();

const logLevel = mapLogLevel(inputLogLevel);
Expand Down
6 changes: 3 additions & 3 deletions src/spec-node/featuresCLI/testCommandImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { DevContainerConfig } from '../../spec-configuration/configuration';
import { FeaturesTestCommandInput } from './test';
import { cpDirectoryLocal, rmLocal } from '../../spec-utils/pfs';
import { nullLog } from '../../spec-utils/log';
import { runCommand } from '../../spec-common/commonUtils';
import { runCommandNoPty } from '../../spec-common/commonUtils';
import { Feature } from '../../spec-configuration/containerFeaturesConfiguration';
import { getSafeId } from '../containerFeatures';

Expand Down Expand Up @@ -81,8 +81,8 @@ export async function doFeaturesTestCommand(args: FeaturesTestCommandInput): Pro
async function cleanup(cliHost: CLIHost) {
// Delete any containers that have the 'devcontainer.is_test_run=true' label set.
const filterForContainerIdArgs = ['ps', '-a', '--filter', 'label=devcontainer.is_test_run=true', '--format', '{{.ID}}'];
const { cmdOutput } = (await runCommand({ cmd: 'docker', args: filterForContainerIdArgs, output: nullLog, ptyExec: cliHost.ptyExec }));
const containerIds = cmdOutput.split('\n').filter(id => id !== '').map(s => s.trim());
const { stdout } = (await runCommandNoPty({ cmd: 'docker', args: filterForContainerIdArgs, output: nullLog, exec: cliHost.exec }));
const containerIds = stdout.toString().split('\n').filter(id => id !== '').map(s => s.trim());
log(`Cleaning up ${containerIds.length} test containers...`, { prefix: '🧹', info: true });
for (const containerId of containerIds) {
log(`Removing container ${containerId}...`, { prefix: '🧹', info: true });
Expand Down
2 changes: 1 addition & 1 deletion src/spec-node/templatesCLI/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ async function templatesPublish({
const pkg = getPackageConfig();

const cwd = process.cwd();
const cliHost = await getCLIHost(cwd, loadNativeModule);
const cliHost = await getCLIHost(cwd, loadNativeModule, true);
const output = createLog({
logLevel: mapLogLevel(inputLogLevel),
logFormat: 'text',
Expand Down
2 changes: 1 addition & 1 deletion src/spec-node/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ export async function createContainerProperties(params: DockerResolverParameters
const [, user, , group] = /([^:]*)(:(.*))?/.exec(containerUser) as (string | undefined)[];
const containerEnv = envListToObj(containerInfo.Config.Env);
const remoteExec = dockerExecFunction(params, containerId, containerUser);
const remotePtyExec = await dockerPtyExecFunction(params, containerId, containerUser, common.loadNativeModule);
const remotePtyExec = await dockerPtyExecFunction(params, containerId, containerUser, common.loadNativeModule, common.allowInheritTTY);
const remoteExecAsRoot = dockerExecFunction(params, containerId, 'root');
return getContainerProperties({
params: common,
Expand Down
11 changes: 8 additions & 3 deletions src/spec-shutdown/dockerUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { CLIHost, runCommand, runCommandNoPty, ExecFunction, ExecParameters, Exec, PtyExecFunction, PtyExec, PtyExecParameters } from '../spec-common/commonUtils';
import { CLIHost, runCommand, runCommandNoPty, ExecFunction, ExecParameters, Exec, PtyExecFunction, PtyExec, PtyExecParameters, plainExecAsPtyExec } from '../spec-common/commonUtils';
import { toErrorText } from '../spec-common/errors';
import * as ptyType from 'node-pty';
import { Log, makeLog } from '../spec-utils/log';
Expand Down Expand Up @@ -63,6 +63,7 @@ export interface PartialExecParameters {

export interface PartialPtyExecParameters {
ptyExec: PtyExecFunction;
exec: ExecFunction; // for fallback operation
cmd: string;
args?: string[];
env: NodeJS.ProcessEnv;
Expand Down Expand Up @@ -318,15 +319,17 @@ export function dockerExecFunction(params: DockerCLIParameters | PartialExecPara
cmd,
args: (args || []).concat(execArgs),
env,
stdio: execParams.stdio,
output: replacingDockerExecLog(execParams.output, cmd, argsPrefix),
});
};
}

export async function dockerPtyExecFunction(params: PartialPtyExecParameters | DockerResolverParameters, containerName: string, user: string | undefined, loadNativeModule: <T>(moduleName: string) => Promise<T | undefined>): Promise<PtyExecFunction> {
export async function dockerPtyExecFunction(params: PartialPtyExecParameters | DockerResolverParameters, containerName: string, user: string | undefined, loadNativeModule: <T>(moduleName: string) => Promise<T | undefined>, allowInheritTTY: boolean): Promise<PtyExecFunction> {
const pty = await loadNativeModule<typeof ptyType>('node-pty');
if (!pty) {
throw new Error('Missing node-pty');
const plain = dockerExecFunction(params, containerName, user);
return plainExecAsPtyExec(plain, allowInheritTTY);
}

return async function (execParams: PtyExecParameters): Promise<PtyExec> {
Expand Down Expand Up @@ -407,12 +410,14 @@ export function toExecParameters(params: DockerCLIParameters | PartialExecParame
export function toPtyExecParameters(params: DockerCLIParameters | PartialPtyExecParameters | DockerResolverParameters, compose?: DockerComposeCLI): PartialPtyExecParameters {
return 'dockerEnv' in params ? {
ptyExec: params.common.cliHost.ptyExec,
exec: params.common.cliHost.exec,
cmd: compose ? compose.cmd : params.dockerCLI,
args: compose ? compose.args : [],
env: params.dockerEnv,
output: params.common.output,
} : 'cliHost' in params ? {
ptyExec: params.cliHost.ptyExec,
exec: params.cliHost.exec,
cmd: compose ? compose.cmd : params.dockerCLI,
args: compose ? compose.args : [],
env: params.env,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ describe('tests packageTemplates()', async function () {
});

const cwd = process.cwd();
const cliHost = await getCLIHost(cwd, loadNativeModule);
const cliHost = await getCLIHost(cwd, loadNativeModule, true);

let args: PackageCommandInput = {
targetFolder: '',
Expand Down
4 changes: 2 additions & 2 deletions src/test/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export interface ExecPtyResult {
}

export async function shellPtyExec(command: string, options: { stdin?: string } = {}): Promise<ExecPtyResult> {
const ptyExec = await plainPtyExec(undefined, loadNativeModule);
const ptyExec = await plainPtyExec(undefined, loadNativeModule, true);
return runCommand({
ptyExec,
cmd: '/bin/sh',
Expand Down Expand Up @@ -147,7 +147,7 @@ export const testSubstitute: SubstituteConfig = value => {
export const output = makeLog(createPlainLog(text => process.stdout.write(text), () => LogLevel.Trace));

export async function createCLIParams(hostPath: string) {
const cliHost = await getCLIHost(hostPath, loadNativeModule);
const cliHost = await getCLIHost(hostPath, loadNativeModule, true);
const dockerComposeCLI = dockerComposeCLIConfig({
exec: cliHost.exec,
env: cliHost.env,
Expand Down