Skip to content

Commit

Permalink
Fix issue with missing interpreter info for some cases (#15376)
Browse files Browse the repository at this point in the history
* Use child process apis directly.

* Use raw API in process service

* Handle process cleanup

* Address sonar

* Refactor process service by pulling the raw process APIs out of the class

* Address comments
  • Loading branch information
karthiknadig authored Feb 11, 2021
1 parent e04b43c commit 3a7c11c
Show file tree
Hide file tree
Showing 6 changed files with 324 additions and 222 deletions.
210 changes: 7 additions & 203 deletions src/client/common/process/proc.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,17 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
import { exec, execSync, spawn } from 'child_process';
import { EventEmitter } from 'events';
import { Observable } from 'rxjs/Observable';
import { Readable } from 'stream';

import { IDisposable } from '../types';
import { createDeferred } from '../utils/async';
import { EnvironmentVariables } from '../variables/types';
import { DEFAULT_ENCODING } from './constants';
import { execObservable, killPid, plainExec, shellExec } from './rawProcessApis';
import {
ExecutionResult,
IBufferDecoder,
IProcessService,
ObservableExecutionResult,
Output,
ShellOptions,
SpawnOptions,
StdErrError,
} from './types';

export class ProcessService extends EventEmitter implements IProcessService {
Expand All @@ -37,16 +31,7 @@ export class ProcessService extends EventEmitter implements IProcessService {
}

public static kill(pid: number): void {
try {
if (process.platform === 'win32') {
// Windows doesn't support SIGTERM, so execute taskkill to kill the process
execSync(`taskkill /pid ${pid} /T /F`); // NOSONAR
} else {
process.kill(pid);
}
} catch {
// Ignore.
}
killPid(pid);
}

public dispose(): void {
Expand All @@ -61,199 +46,18 @@ export class ProcessService extends EventEmitter implements IProcessService {
}

public execObservable(file: string, args: string[], options: SpawnOptions = {}): ObservableExecutionResult<string> {
const spawnOptions = this.getDefaultOptions(options);
const encoding = spawnOptions.encoding ? spawnOptions.encoding : 'utf8';
const proc = spawn(file, args, spawnOptions);
let procExited = false;
const disposable: IDisposable = {
dispose() {
if (proc && !proc.killed && !procExited) {
ProcessService.kill(proc.pid);
}
if (proc) {
proc.unref();
}
},
};
this.processesToKill.add(disposable);

const output = new Observable<Output<string>>((subscriber) => {
const disposables: IDisposable[] = [];

// eslint-disable-next-line @typescript-eslint/ban-types
const on = (ee: Readable | null, name: string, fn: Function) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ee?.on(name, fn as any);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
disposables.push({ dispose: () => ee?.removeListener(name, fn as any) as any });
};

if (options.token) {
disposables.push(
options.token.onCancellationRequested(() => {
if (!procExited && !proc.killed) {
proc.kill();
procExited = true;
}
}),
);
}

const sendOutput = (source: 'stdout' | 'stderr', data: Buffer) => {
const out = this.decoder.decode([data], encoding);
if (source === 'stderr' && options.throwOnStdErr) {
subscriber.error(new StdErrError(out));
} else {
subscriber.next({ source, out });
}
};

on(proc.stdout, 'data', (data: Buffer) => sendOutput('stdout', data));
on(proc.stderr, 'data', (data: Buffer) => sendOutput('stderr', data));

proc.once('close', () => {
procExited = true;
subscriber.complete();
disposables.forEach((d) => d.dispose());
});
proc.once('exit', () => {
procExited = true;
subscriber.complete();
disposables.forEach((d) => d.dispose());
});
proc.once('error', (ex) => {
procExited = true;
subscriber.error(ex);
disposables.forEach((d) => d.dispose());
});
});

const result = execObservable(file, args, options, this.decoder, this.env, this.processesToKill);
this.emit('exec', file, args, options);

return {
proc,
out: output,
dispose: disposable.dispose,
};
return result;
}

public exec(file: string, args: string[], options: SpawnOptions = {}): Promise<ExecutionResult<string>> {
const spawnOptions = this.getDefaultOptions(options);
const encoding = spawnOptions.encoding ? spawnOptions.encoding : 'utf8';
const proc = spawn(file, args, spawnOptions);
const deferred = createDeferred<ExecutionResult<string>>();
const disposable: IDisposable = {
dispose: () => {
if (!proc.killed && !deferred.completed) {
proc.kill();
}
},
};
this.processesToKill.add(disposable);
const disposables: IDisposable[] = [];

// eslint-disable-next-line @typescript-eslint/ban-types
const on = (ee: Readable | null, name: string, fn: Function) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ee?.on(name, fn as any);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
disposables.push({ dispose: () => ee?.removeListener(name, fn as any) as any });
};

if (options.token) {
disposables.push(options.token.onCancellationRequested(disposable.dispose));
}

const stdoutBuffers: Buffer[] = [];
on(proc.stdout, 'data', (data: Buffer) => stdoutBuffers.push(data));
const stderrBuffers: Buffer[] = [];
on(proc.stderr, 'data', (data: Buffer) => {
if (options.mergeStdOutErr) {
stdoutBuffers.push(data);
stderrBuffers.push(data);
} else {
stderrBuffers.push(data);
}
});

proc.once('close', () => {
if (deferred.completed) {
return;
}
const stderr: string | undefined =
stderrBuffers.length === 0 ? undefined : this.decoder.decode(stderrBuffers, encoding);
if (stderr && stderr.length > 0 && options.throwOnStdErr) {
deferred.reject(new StdErrError(stderr));
} else {
const stdout = this.decoder.decode(stdoutBuffers, encoding);
deferred.resolve({ stdout, stderr });
}
disposables.forEach((d) => d.dispose());
});
proc.once('error', (ex) => {
deferred.reject(ex);
disposables.forEach((d) => d.dispose());
});

const promise = plainExec(file, args, options, this.decoder, this.env, this.processesToKill);
this.emit('exec', file, args, options);

return deferred.promise;
return promise;
}

public shellExec(command: string, options: ShellOptions = {}): Promise<ExecutionResult<string>> {
const shellOptions = this.getDefaultOptions(options);
return new Promise((resolve, reject) => {
const proc = exec(command, shellOptions, (e, stdout, stderr) => {
if (e && e !== null) {
reject(e);
} else if (shellOptions.throwOnStdErr && stderr && stderr.length) {
reject(new Error(stderr));
} else {
// Make sure stderr is undefined if we actually had none. This is checked
// elsewhere because that's how exec behaves.
resolve({ stderr: stderr && stderr.length > 0 ? stderr : undefined, stdout });
}
}); // NOSONAR
const disposable: IDisposable = {
dispose: () => {
if (!proc.killed) {
proc.kill();
}
},
};
this.processesToKill.add(disposable);
});
}

private getDefaultOptions<T extends ShellOptions | SpawnOptions>(options: T): T {
const defaultOptions = { ...options };
const execOptions = defaultOptions as SpawnOptions;
if (execOptions) {
execOptions.encoding =
typeof execOptions.encoding === 'string' && execOptions.encoding.length > 0
? execOptions.encoding
: DEFAULT_ENCODING;
const { encoding } = execOptions;
delete execOptions.encoding;
execOptions.encoding = encoding;
}
if (!defaultOptions.env || Object.keys(defaultOptions.env).length === 0) {
const env = this.env ? this.env : process.env;
defaultOptions.env = { ...env };
} else {
defaultOptions.env = { ...defaultOptions.env };
}

if (execOptions && execOptions.extraVariables) {
defaultOptions.env = { ...defaultOptions.env, ...execOptions.extraVariables };
}

// Always ensure we have unbuffered output.
defaultOptions.env.PYTHONUNBUFFERED = '1';
if (!defaultOptions.env.PYTHONIOENCODING) {
defaultOptions.env.PYTHONIOENCODING = 'utf-8';
}

return defaultOptions;
return shellExec(command, options, this.env, this.processesToKill);
}
}
Loading

0 comments on commit 3a7c11c

Please sign in to comment.