app/@overflow/core/shell.ts
crusader 4695d7042c ing
2018-10-02 14:03:43 +09:00

168 lines
4.0 KiB
TypeScript

/* eslint-disable no-sync */
import * as stream from 'stream';
import * as ChildProcess from 'child_process';
import * as os from 'os';
interface IndexLookup {
[propName: string]: string;
}
/**
* The names of any env vars that we shouldn't copy from the shell environment.
*/
const BlacklistedNames = new Set(['LOCAL_GIT_DIRECTORY']);
/**
* Inspect whether the current process needs to be patched to get important
* environment variables for Desktop to work and integrate with other tools
* the user may invoke as part of their workflow.
*
* This is only applied to macOS installations due to how the application
* is launched.
*
* @param process The process to inspect.
*/
export function shellNeedsPatching(process: NodeJS.Process): boolean {
return __DARWIN__ && !process.env.PWD;
}
interface ShellResult {
stdout: string;
error: Error | null;
}
/**
* Gets a dump of the user's configured shell environment.
*
* @returns the output of the `env` command or `null` if there was an error.
*/
async function getRawShellEnv(): Promise<string | null> {
const shell = getUserShell();
const promise = new Promise<ShellResult>(resolve => {
let child: ChildProcess.ChildProcess | null = null;
let _error: Error | null = null;
let _stdout = '';
let done = false;
// ensure we clean up eventually, in case things go bad
const cleanup = () => {
if (!done && child) {
child.kill();
done = true;
}
};
process.once('exit', cleanup);
setTimeout(() => {
cleanup();
}, 5000);
const options: ChildProcess.SpawnOptions = {
detached: true,
stdio: ['ignore', 'pipe', process.stderr as stream.Stream]
};
child = ChildProcess.spawn(shell, ['-ilc', 'command env'], options);
const buffers: Array<Buffer> = [];
child.on('error', (e: Error) => {
done = true;
_error = e;
});
child.stdout.on('data', (data: Buffer) => {
buffers.push(data);
});
child.on('close', (code: number, signal) => {
done = true;
process.removeListener('exit', cleanup);
if (buffers.length) {
_stdout = Buffer.concat(buffers).toString('utf8');
}
resolve({ stdout: _stdout, error });
});
});
const { stdout, error } = await promise;
if (error) {
// just swallow the error and move on with everything
return null;
}
return stdout;
}
function getUserShell() {
if (process.env.SHELL) {
return process.env.SHELL;
}
return '/bin/bash';
}
/**
* Get the environment variables from the user's current shell and update the
* current environment.
*
* @param updateEnvironment a callback to fire if a valid environment is found
*/
async function getEnvironmentFromShell(
updateEnvironment: (env: IndexLookup) => void
): Promise<void> {
if (__WIN32__) {
return;
}
const shellEnvText = await getRawShellEnv();
if (!shellEnvText) {
return;
}
const env: IndexLookup = {};
for (const line of shellEnvText.split(os.EOL)) {
if (line.includes('=')) {
const components = line.split('=');
if (components.length === 2) {
env[components[0]] = components[1];
} else {
const k = components.shift();
const v = components.join('=');
if (k) {
env[k] = v;
}
}
}
}
updateEnvironment(env);
}
/**
* Apply new environment variables to the current process, ignoring
* Node-specific environment variables which need to be preserved.
*
* @param env The new environment variables from the user's shell.
*/
function mergeEnvironmentVariables(env: IndexLookup) {
for (const key in env) {
if (BlacklistedNames.has(key)) {
continue;
}
process.env[key] = env[key];
}
}
/**
* Update the current process's environment variables using environment
* variables from the user's shell, if they can be retrieved successfully.
*/
export function updateEnvironmentForProcess(): Promise<void> {
return getEnvironmentFromShell(mergeEnvironmentVariables);
}